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).');