diff --git a/.gitignore b/.gitignore
index 3fdb568ac0..b8eac63118 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,6 +66,13 @@ apps/swift/xcconfig/*.local.xcconfig
apps/swift/PackRat.xcodeproj/
apps/swift/*.xcworkspace/xcuserdata/
apps/swift/DerivedData*/
+apps/swift/build/
+apps/swift/build.log
+apps/swift/upload.log
+# Signing material ā never commit private keys / certs (fastlane cert output)
+apps/swift/*.p12
+apps/swift/*.cer
+apps/swift/*.certSigningRequest
apps/swift/.build/
apps/swift/.swiftpm/
apps/swift/Package.resolved
diff --git a/apps/swift/Resources/Info-iOS.plist b/apps/swift/Resources/Info-iOS.plist
index 1d5666e420..54f489dbca 100644
--- a/apps/swift/Resources/Info-iOS.plist
+++ b/apps/swift/Resources/Info-iOS.plist
@@ -47,10 +47,10 @@
CFBundleVersion
1
- ITSAppUsesNonExemptEncryption
-
GOOGLE_IOS_CLIENT_ID
993694750638-97t0vhfml04u2avrlbve22jbs9qcinbc.apps.googleusercontent.com
+ ITSAppUsesNonExemptEncryption
+
LSRequiresIPhoneOS
NSAppTransportSecurity
@@ -74,5 +74,12 @@
UIColorName
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
diff --git a/apps/swift/Resources/Info-watchOS.plist b/apps/swift/Resources/Info-watchOS.plist
index 87768fc109..0333ddd38e 100644
--- a/apps/swift/Resources/Info-watchOS.plist
+++ b/apps/swift/Resources/Info-watchOS.plist
@@ -25,6 +25,6 @@
WKApplication
WKCompanionAppBundleIdentifier
- com.andrewbierman.packrat
+ com.andrewbierman.packrat.swift
diff --git a/apps/swift/Sources/PackRat/PackRatApp.swift b/apps/swift/Sources/PackRat/PackRatApp.swift
index 138a89eb5c..06c61fffc1 100644
--- a/apps/swift/Sources/PackRat/PackRatApp.swift
+++ b/apps/swift/Sources/PackRat/PackRatApp.swift
@@ -12,6 +12,10 @@ struct PackRatApp: App {
#endif
init() {
+ // Telemetry has to start before any view is mounted so launch-time
+ // errors are captured. A missing DSN silently disables the SDK.
+ SentryConfig.start()
+
#if os(macOS)
if ProcessInfo.processInfo.arguments.contains("--reset-auth") {
UserDefaults.standard.set(true, forKey: "ApplePersistenceIgnoreState")
@@ -20,12 +24,6 @@ struct PackRatApp: App {
#endif
}
- init() {
- // Telemetry has to start before any view is mounted so launch-time
- // errors are captured. A missing DSN silently disables the SDK.
- SentryConfig.start()
- }
-
var body: some Scene {
WindowGroup {
AuthGateView()
diff --git a/apps/swift/project.yml b/apps/swift/project.yml
index 0a0219047d..d21a13351d 100644
--- a/apps/swift/project.yml
+++ b/apps/swift/project.yml
@@ -59,7 +59,6 @@ targets:
sources:
- Sources/PackRat
- Sources/PackRatShared
- resources:
- Resources/Assets.xcassets
entitlements:
path: Resources/PackRat-iOS.entitlements
@@ -71,6 +70,13 @@ targets:
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
LSRequiresIPhoneOS: true
+ # All four orientations are required for iPad (device family "1,2");
+ # App Store validation (error 90474) rejects the bundle otherwise.
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ - UIInterfaceOrientationPortraitUpsideDown
+ - UIInterfaceOrientationLandscapeLeft
+ - UIInterfaceOrientationLandscapeRight
UILaunchScreen:
UIColorName: ""
UIApplicationSceneManifest:
@@ -117,17 +123,20 @@ targets:
product: Sentry
- package: GoogleSignIn
product: GoogleSignIn
- - target: PackRat-Watch
+ # PackRat-Watch is intentionally NOT embedded in distribution builds yet:
+ # the watch app has no app icons, which App Store validation rejects
+ # (error 90391). Re-add this dependency once watch icons exist.
settings:
base:
SWIFT_VERSION: "5.9"
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
CODE_SIGN_STYLE: Automatic
- DEVELOPMENT_TEAM: 7WV9JYCW55
- PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat
+ DEVELOPMENT_TEAM: 666HGMV2LU
+ PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.swift
PRODUCT_MODULE_NAME: PackRat
TARGETED_DEVICE_FAMILY: "1,2"
+ ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
PackRat-Watch:
type: application
@@ -143,7 +152,7 @@ targets:
CFBundleShortVersionString: "1.0"
CFBundleVersion: "1"
WKApplication: true
- WKCompanionAppBundleIdentifier: com.andrewbierman.packrat
+ WKCompanionAppBundleIdentifier: com.andrewbierman.packrat.swift
ITSAppUsesNonExemptEncryption: false
settings:
base:
@@ -151,8 +160,8 @@ targets:
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
CODE_SIGN_STYLE: Automatic
- DEVELOPMENT_TEAM: 7WV9JYCW55
- PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.watchkitapp
+ DEVELOPMENT_TEAM: 666HGMV2LU
+ PRODUCT_BUNDLE_IDENTIFIER: com.andrewbierman.packrat.swift.watchkitapp
PRODUCT_MODULE_NAME: PackRatWatch
PackRat-macOS:
diff --git a/apps/swift/scripts/upload-testflight.ts b/apps/swift/scripts/upload-testflight.ts
new file mode 100644
index 0000000000..169db5c771
--- /dev/null
+++ b/apps/swift/scripts/upload-testflight.ts
@@ -0,0 +1,126 @@
+#!/usr/bin/env bun
+/**
+ * Archive the native Swift PackRat iOS app and upload it to TestFlight.
+ *
+ * This targets a SEPARATE App Store Connect record from the production Expo
+ * app: bundle id `com.andrewbierman.packrat.swift`. Register that app record
+ * in App Store Connect once before the first upload.
+ *
+ * Auth uses an Apple ID + app-specific password (no App Store Connect API key
+ * required). Generate a password at appleid.apple.com -> Sign-In & Security ->
+ * App-Specific Passwords.
+ *
+ * Required env (put in apps/swift/.env.local, gitignored):
+ * APPLE_ID your Apple ID email
+ * APPLE_APP_PASSWORD app-specific password (xxxx-xxxx-xxxx-xxxx)
+ * APPLE_TEAM_ID the team that owns the record (e.g. 7WV9JYCW55)
+ *
+ * Optional env:
+ * BUILD_NUMBER CFBundleVersion for this upload (default: timestamp)
+ *
+ * Usage:
+ * bun apps/swift/scripts/upload-testflight.ts
+ */
+import { execFileSync } from 'node:child_process';
+import { mkdtempSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+const SWIFT_DIR = new URL('..', import.meta.url).pathname;
+const PROJECT = join(SWIFT_DIR, 'PackRat.xcodeproj');
+const SCHEME = 'PackRat-iOS';
+const BUNDLE_ID = 'com.andrewbierman.packrat.swift';
+
+function req(name: string): string {
+ const v = process.env[name];
+ if (!v) {
+ console.error(`Missing required env var: ${name}. See script header.`);
+ process.exit(1);
+ }
+ return v;
+}
+
+const appleId = req('APPLE_ID');
+const appPassword = req('APPLE_APP_PASSWORD');
+const teamId = req('APPLE_TEAM_ID');
+const buildNumber = process.env.BUILD_NUMBER ?? String(Math.floor(Date.now() / 1000));
+
+const work = mkdtempSync(join(tmpdir(), 'packrat-tf-'));
+const archivePath = join(work, 'PackRat.xcarchive');
+const exportDir = join(work, 'export');
+
+function run(cmd: string, args: string[]) {
+ console.log(`\n$ ${cmd} ${args.join(' ')}`);
+ execFileSync(cmd, args, { stdio: 'inherit' });
+}
+
+// 1. Archive for a real device (TestFlight cannot accept a simulator build).
+run('xcodebuild', [
+ 'archive',
+ '-project',
+ PROJECT,
+ '-scheme',
+ SCHEME,
+ '-destination',
+ 'generic/platform=iOS',
+ '-archivePath',
+ archivePath,
+ // Lets Xcode register the App IDs and generate provisioning profiles for
+ // the (new) bundle ids on the fly, using the signed-in account.
+ '-allowProvisioningUpdates',
+ `CURRENT_PROJECT_VERSION=${buildNumber}`,
+ `DEVELOPMENT_TEAM=${teamId}`,
+]);
+
+// 2. Export a signed .ipa for App Store distribution.
+const exportOptions = join(work, 'ExportOptions.plist');
+writeFileSync(
+ exportOptions,
+ `
+
+
+
+ methodapp-store-connect
+ teamID${teamId}
+ destinationexport
+ signingStyleautomatic
+ uploadSymbols
+
+
+`,
+);
+
+run('xcodebuild', [
+ '-exportArchive',
+ '-archivePath',
+ archivePath,
+ '-exportPath',
+ exportDir,
+ '-exportOptionsPlist',
+ exportOptions,
+ // Export also needs to generate the App Store distribution profiles for the
+ // new bundle ids on the fly.
+ '-allowProvisioningUpdates',
+]);
+
+// 3. Upload to TestFlight via altool (app-specific-password auth).
+// `--asc-provider` (team short name) is required when the Apple ID belongs to
+// more than one team, so altool knows which one to deliver to.
+const ipa = join(exportDir, 'PackRat-iOS.ipa');
+run('xcrun', [
+ 'altool',
+ '--upload-app',
+ '--type',
+ 'ios',
+ '--file',
+ ipa,
+ '--username',
+ appleId,
+ '--password',
+ appPassword,
+ '--asc-provider',
+ teamId,
+]);
+
+console.log(`\nā Uploaded build ${buildNumber} to TestFlight (${BUNDLE_ID}).`);
+console.log('It will appear in App Store Connect after processing (usually 5-15 min).');