iOS 스토어 제출 구성 · Apple 로그인 · 푸시 · Firebase 설정 연결
This commit is contained in:
@@ -55,6 +55,8 @@ key.properties
|
||||
*.mobileprovision
|
||||
**/GoogleService-Info.plist
|
||||
**/google-services.json
|
||||
# GoogleService-Info.plist 에서 생성한다 — tool/ios/inject_url_scheme.py
|
||||
ios/config/Google.xcconfig
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
|
||||
+26
-2
@@ -100,8 +100,32 @@ export DEVELOPER_DIR=~/Downloads/Xcode-beta.app/Contents/Developer
|
||||
fvm flutter build ios --release --no-codesign
|
||||
```
|
||||
|
||||
**서명은 Apple Developer 계정이 있어야 한다.** 계정 없이 여기까지는 확인되므로,
|
||||
TestFlight 제출에 남은 것은 **서명과 업로드뿐**이다.
|
||||
### 제출 빌드 — App Store Connect
|
||||
|
||||
서명 팀은 **Fancake Inc. `V28G543VK5`** 다(`Runner.xcodeproj` 의 `DEVELOPMENT_TEAM`,
|
||||
`ios/ExportOptions.plist` 의 `teamID`). 번들 ID `kr.co.fanuzu.app`.
|
||||
|
||||
| 항목 | 파일 | 없으면 |
|
||||
|---|---|---|
|
||||
| Sign in with Apple · 푸시 환경 | `ios/Runner/Runner.entitlements` | Apple 로그인이 동작하지 않고 심사에서 반려된다 |
|
||||
| 암호화 사용 신고 | `Info.plist` `ITSAppUsesNonExemptEncryption` | 업로드마다 수동 응답을 요구한다 |
|
||||
| Google 콜백 스킴 | `Info.plist` + `ios/config/Google.xcconfig` | Google 로그인 콜백이 돌아오지 않는다 |
|
||||
| Firebase 설정 | `ios/Runner/GoogleService-Info.plist` (번들 리소스) | 목 모드로 뜨고 Google 로그인 client ID 를 못 읽는다 |
|
||||
| 내보내기 설정 | `ios/ExportOptions.plist` | `-exportArchive` 가 대화형으로 묻는다 |
|
||||
|
||||
`GoogleService-Info.plist` 와 `ios/config/Google.xcconfig` 는 저장소에 넣지 않는다.
|
||||
Xcode 프로젝트가 `GoogleService-Info.plist` 를 리소스로 참조하므로 **파일이 없으면 iOS 빌드가 멈춘다.**
|
||||
|
||||
```bash
|
||||
# 1. Firebase 콘솔에서 받은 iOS 설정 파일을 둔다
|
||||
cp <받은 파일> ios/Runner/GoogleService-Info.plist
|
||||
# 2. Google 콜백 스킴을 뽑는다 → ios/config/Google.xcconfig
|
||||
python3 tool/ios/inject_url_scheme.py
|
||||
# 3. 아카이브 · IPA
|
||||
fvm flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
|
||||
```
|
||||
|
||||
배포 프로파일이 없으면 `xcodebuild` 로 나눠 `-allowProvisioningUpdates` 를 붙인다.
|
||||
|
||||
### 정리해야 할 것 — CocoaPods / SwiftPM 이중 상태
|
||||
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|
||||
|---|---|---|---|
|
||||
| S1 | Firebase 프로젝트(dev/prod) 개설 | — | 아래 S2 를 만들 수 없다 |
|
||||
| S2 | `google-services.json` | `app/android/app/google-services.json` | 목 모드로 기동. Google 로그인 차단 |
|
||||
| S2 | `GoogleService-Info.plist` | `app/ios/Runner/GoogleService-Info.plist` | 목 모드로 기동. Google·Apple 로그인 차단 |
|
||||
| S2 | `GoogleService-Info.plist` | `app/ios/Runner/GoogleService-Info.plist` | iOS 빌드가 멈춘다(번들 리소스로 참조). 투입 절차는 `BUILD.md` 「제출 빌드」 |
|
||||
|
||||
> S2 투입 시 Android 는 `android/settings.gradle.kts` 에 `com.google.gms.google-services` 플러그인
|
||||
> 추가가 함께 필요하다. 지금은 설정 파일이 없어 플러그인을 적용하지 않았다 —
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<!--
|
||||
TestFlight · App Store 업로드용 내보내기 설정.
|
||||
|
||||
`xcodebuild -exportArchive` 가 이 파일을 읽는다. 없으면 대화형으로 물어보므로
|
||||
자동 배포가 불가능하다 — 계정이 열린 뒤에 만들면 그날 하루를 여기서 쓴다.
|
||||
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
|
||||
<!-- Fancake Inc. -->
|
||||
<key>teamID</key>
|
||||
<string>V28G543VK5</string>
|
||||
|
||||
<!--
|
||||
Xcode 가 프로비저닝 프로파일을 알아서 만들고 갱신한다. 수동으로 두면
|
||||
인증서가 만료될 때마다 배포가 멈춘다.
|
||||
-->
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
|
||||
<!-- 심벌을 함께 올려야 크래시 로그가 사람이 읽을 수 있는 형태로 온다. -->
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
|
||||
<!-- 비트코드는 Xcode 14 부터 폐지되었다. -->
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
|
||||
<!--
|
||||
앱 씬 크기 최적화. 기기별로 필요한 자산만 내려가 설치 용량이 줄어든다.
|
||||
-->
|
||||
<key>thinning</key>
|
||||
<string><thin-for-all-variants></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,2 +1,6 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
|
||||
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
|
||||
#include? "../config/Google.xcconfig"
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
|
||||
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
|
||||
#include? "../config/Google.xcconfig"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
87BFB00ABC23EA249E841B59 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CBE157220A7C382F845775F0 /* Pods_Runner.framework */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
4F2B0E1A2C3D4E5F60718294 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
EC46D318C684524E6F9C925F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */; };
|
||||
@@ -64,6 +65,7 @@
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A68C3B80D45981965B89CA40 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -149,6 +151,7 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
@@ -275,6 +278,7 @@
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
4F2B0E1A2C3D4E5F60718294 /* GoogleService-Info.plist in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -467,7 +471,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEVELOPMENT_TEAM = V28G543VK5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -650,7 +655,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEVELOPMENT_TEAM = V28G543VK5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -673,7 +679,8 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
DEVELOPMENT_TEAM = V28G543VK5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
||||
@@ -66,5 +66,29 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!--
|
||||
암호화 사용 신고. HTTPS 만 쓰므로 면제 대상이다.
|
||||
이 키가 없으면 업로드할 때마다 App Store Connect 가 수동 응답을 요구한다.
|
||||
-->
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
|
||||
<!--
|
||||
Google 로그인 콜백 URL 스킴. 값은 GoogleService-Info.plist 의 REVERSED_CLIENT_ID 이며
|
||||
tool/ios/inject_url_scheme.py 가 ios/config/Google.xcconfig 로 뽑아 둔다.
|
||||
-->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>google-sign-in</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>$(GOOGLE_REVERSED_CLIENT_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!--
|
||||
Sign in with Apple. 소셜 로그인을 제공하면 Apple 심사 필수 조건이다.
|
||||
이 Capability 가 없으면 Apple 로그인 코드가 있어도 동작하지 않는다.
|
||||
-->
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
|
||||
<!--
|
||||
푸시 환경 선언. Firebase Auth 가 APNs 등록 API 를 링크하므로 선언해 둔다.
|
||||
App Store 내보내기 때 Xcode 가 production 으로 바꿔 서명한다.
|
||||
-->
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// iOS 스토어 제출 구성 검사.
|
||||
///
|
||||
/// 여기 있는 것들은 사라져도 앱이 멀쩡히 빌드된다. 문제는 업로드·심사·로그인
|
||||
/// 시점에야 드러나고, 그때는 원인이 코드처럼 보인다. 파일을 문자열로 읽어
|
||||
/// 키가 있는지만 본다 — 값의 해석은 빌드가 한다.
|
||||
|
||||
const _teamId = 'V28G543VK5'; // Fancake Inc.
|
||||
|
||||
String _read(String path) {
|
||||
final file = File(path);
|
||||
expect(file.existsSync(), isTrue, reason: '$path 가 없습니다');
|
||||
return file.readAsStringSync();
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('iOS — 서명', () {
|
||||
test('Xcode 프로젝트와 내보내기 설정이 발주사 팀으로 서명한다', () {
|
||||
final pbx = _read('ios/Runner.xcodeproj/project.pbxproj');
|
||||
final teams = RegExp(r'DEVELOPMENT_TEAM = (\w+);')
|
||||
.allMatches(pbx)
|
||||
.map((m) => m.group(1))
|
||||
.toSet();
|
||||
expect(teams, {_teamId});
|
||||
expect(_read('ios/ExportOptions.plist'), contains(_teamId));
|
||||
});
|
||||
|
||||
test('Sign in with Apple Capability 가 서명에 연결되어 있다', () {
|
||||
expect(
|
||||
_read('ios/Runner/Runner.entitlements'),
|
||||
contains('com.apple.developer.applesignin'),
|
||||
);
|
||||
expect(
|
||||
'CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;'
|
||||
.allMatches(_read('ios/Runner.xcodeproj/project.pbxproj'))
|
||||
.length,
|
||||
3, // Debug · Release · Profile
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('iOS — 제출 선언과 Firebase 설정', () {
|
||||
late final String plist = _read('ios/Runner/Info.plist');
|
||||
|
||||
test('암호화 사용 신고가 있어 업로드가 멈추지 않는다', () {
|
||||
expect(plist, contains('ITSAppUsesNonExemptEncryption'));
|
||||
});
|
||||
|
||||
test('Google 로그인 콜백 스킴이 설정 파일 값으로 채워진다', () {
|
||||
expect(plist, contains(r'$(GOOGLE_REVERSED_CLIENT_ID)'));
|
||||
for (final c in const ['Debug', 'Release']) {
|
||||
expect(
|
||||
_read('ios/Flutter/$c.xcconfig'),
|
||||
contains('#include? "../config/Google.xcconfig"'),
|
||||
reason: c,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('GoogleService-Info.plist 가 앱 번들 리소스로 들어간다', () {
|
||||
// 번들에 없으면 Firebase 가 목 모드로 뜨고 Google 로그인 client ID 도 못 읽는다.
|
||||
expect(
|
||||
_read('ios/Runner.xcodeproj/project.pbxproj'),
|
||||
contains('/* GoogleService-Info.plist in Resources */,'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('설정·서명 자산을 git 이 추적하지 않는다', () {
|
||||
final result = Process.runSync('git', [
|
||||
'ls-files',
|
||||
'--',
|
||||
'ios/Runner/GoogleService-Info.plist',
|
||||
'ios/config/Google.xcconfig',
|
||||
'android/app/google-services.json',
|
||||
'*.p12',
|
||||
'*.p8',
|
||||
'*.mobileprovision',
|
||||
]);
|
||||
expect(result.exitCode, 0, reason: 'git 을 실행하지 못했습니다');
|
||||
expect((result.stdout as String).trim(), isEmpty);
|
||||
});
|
||||
}
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""GoogleService-Info.plist 의 REVERSED_CLIENT_ID 를 Xcode 빌드 설정에 넣는다.
|
||||
|
||||
**왜 스크립트인가.** Google 로그인 콜백 스킴은 Firebase 프로젝트마다 다르고,
|
||||
Info.plist 에 손으로 옮기면 오타 한 글자에 로그인이 **조용히** 실패한다.
|
||||
오류도 뜨지 않고 콜백만 돌아오지 않아 원인을 찾는 데 한나절이 든다.
|
||||
|
||||
설정 파일이 투입될 때 한 번 돌리면 `ios/config/Google.xcconfig` 가 만들어지고,
|
||||
Info.plist 의 `$(GOOGLE_REVERSED_CLIENT_ID)` 가 그 값으로 치환된다.
|
||||
|
||||
실행: python3 tool/ios/inject_url_scheme.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "ios/Runner/GoogleService-Info.plist"
|
||||
OUT = ROOT / "ios/config/Google.xcconfig"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SOURCE.exists():
|
||||
print(
|
||||
f"설정 파일이 아직 없습니다: {SOURCE.relative_to(ROOT)}\n"
|
||||
"Firebase 콘솔에서 iOS 앱 설정 파일을 받아 그 자리에 두십시오.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
with SOURCE.open("rb") as f:
|
||||
data = plistlib.load(f)
|
||||
|
||||
reversed_id = data.get("REVERSED_CLIENT_ID")
|
||||
if not reversed_id:
|
||||
# Firebase 콘솔에서 Google 로그인을 켜지 않으면 이 키가 빠진 채로 나온다.
|
||||
print(
|
||||
"REVERSED_CLIENT_ID 가 없습니다. Firebase 콘솔에서 "
|
||||
"Authentication → Google 제공자를 켠 뒤 설정 파일을 다시 받으십시오.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT.write_text(
|
||||
"// 자동 생성 — tool/ios/inject_url_scheme.py\n"
|
||||
"// GoogleService-Info.plist 가 바뀌면 다시 돌리십시오.\n"
|
||||
f"GOOGLE_REVERSED_CLIENT_ID = {reversed_id}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"기록: {OUT.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user