From 65f1daa9958334dfeee0f22de22b755c4a69b0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Wed, 18 Feb 2026 10:11:16 +0100 Subject: [PATCH] iOS build fixes/prep for TestFlight --- .github/workflows/ios-testflight.yml | 172 ++++++++++++++++++++ mobile/README.md | 3 + mobile/docs/iOS_TESTFLIGHT.md | 47 ++++++ mobile/ios/Flutter/Debug.xcconfig | 2 +- mobile/ios/Flutter/Profile.xcconfig | 2 + mobile/ios/Flutter/Release.xcconfig | 2 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 26 ++- 7 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ios-testflight.yml create mode 100644 mobile/docs/iOS_TESTFLIGHT.md create mode 100644 mobile/ios/Flutter/Profile.xcconfig diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml new file mode 100644 index 000000000..8bf946836 --- /dev/null +++ b/.github/workflows/ios-testflight.yml @@ -0,0 +1,172 @@ +name: iOS TestFlight + +on: + workflow_dispatch: + inputs: + notes: + description: "TestFlight release notes" + required: false + type: string + push: + tags: + - 'ios-v*' + +permissions: + contents: read + +jobs: + build-and-upload: + name: Build signed IPA and upload to TestFlight + runs-on: macos-latest + timeout-minutes: 120 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.32.4' + channel: 'stable' + cache: true + + - name: Install Flutter dependencies + working-directory: mobile + run: flutter pub get + + - name: Copy app icon source + run: cp public/android-chrome-512x512.png mobile/assets/icon/app_icon.png + + - name: Generate app icons + working-directory: mobile + run: flutter pub run flutter_launcher_icons + + - name: Install CocoaPods + working-directory: mobile/ios + run: pod install + + - name: Configure iOS signing assets + id: signing + env: + KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }} + DIST_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_P12_BASE64 }} + DIST_P12_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_P12_PASSWORD }} + PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }} + PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }} + run: | + set -euo pipefail + + KEYCHAIN_PATH="$RUNNER_TEMP/ios-signing.keychain-db" + PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 3600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" + + CERT_PATH="$RUNNER_TEMP/distribution.p12" + echo "$DIST_P12_BASE64" | base64 --decode > "$CERT_PATH" + security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$DIST_P12_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + if [ -n "$PROFILE_BASE64" ] && [ -n "$PROFILE_NAME" ]; then + mkdir -p "$PROFILE_DIR" + PROFILE_PATH="$PROFILE_DIR/${PROFILE_NAME}.mobileprovision" + echo "$PROFILE_BASE64" | base64 --decode > "$PROFILE_PATH" + fi + + rm -f "$CERT_PATH" + echo "keychain-path=$KEYCHAIN_PATH" >> "$GITHUB_OUTPUT" + + - name: Build signed IPA + working-directory: mobile + env: + APP_BUNDLE_ID: am.sure.mobile + IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }} + PROFILE_NAME: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }} + IOS_DISTRIBUTION_CERT_NAME: ${{ secrets.IOS_DISTRIBUTION_CERT_NAME }} + run: | + set -euo pipefail + + EXPORT_PLIST="$RUNNER_TEMP/ExportOptions.plist" + cat > "$EXPORT_PLIST" < + + + + method + app-store + teamID + ${IOS_TEAM_ID} + signingStyle + manual + provisioningProfiles + + ${APP_BUNDLE_ID} + ${PROFILE_NAME} + + uploadBitcode + + uploadSymbols + + compileBitcode + + + + EOF + + CODE_SIGN_IDENTITY="${IOS_DISTRIBUTION_CERT_NAME}" \ + CODE_SIGN_STYLE=Manual \ + DEVELOPMENT_TEAM="${IOS_TEAM_ID}" \ + PROVISIONING_PROFILE_SPECIFIER="${PROFILE_NAME}" \ + flutter build ipa --release --export-options-plist="$EXPORT_PLIST" + + - name: Prepare TestFlight auth key + id: testflight_key + env: + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }} + run: | + set -euo pipefail + KEY_FILE="$RUNNER_TEMP/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" + echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$KEY_FILE" + echo "key-file=$KEY_FILE" >> "$GITHUB_OUTPUT" + + - name: Upload IPA to TestFlight + working-directory: mobile + env: + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY_FILE: ${{ steps.testflight_key.outputs.key-file }} + run: | + set -euo pipefail + IPA_PATH="$(find build/ios/ipa -name '*.ipa' | head -n 1)" + if [ -z "$IPA_PATH" ]; then + echo "::error::No IPA found at build/ios/ipa" + exit 1 + fi + + xcrun altool \ + --upload-app \ + --file "$IPA_PATH" \ + --type ios \ + --apiKey "$APP_STORE_CONNECT_API_KEY_ID" \ + --apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID" \ + --apiPrivateKey "$APP_STORE_CONNECT_API_KEY_FILE" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ios-ipa-testflight + path: mobile/build/ios/ipa/*.ipa + if-no-files-found: error + + - name: Cleanup signing keychain + if: always() + run: | + set -euo pipefail + KEYCHAIN_PATH="${{ steps.signing.outputs.keychain-path }}" + if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then + security delete-keychain "$KEYCHAIN_PATH" + fi diff --git a/mobile/README.md b/mobile/README.md index 1e3287710..8065766b6 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -151,6 +151,9 @@ The app includes automated CI/CD via GitHub Actions (`.github/workflows/flutter- - **Android Build**: Generates release APK and AAB artifacts - **iOS Build**: Generates iOS release build (unsigned) - **Quality Checks**: Code analysis and tests run before building +- **TestFlight**: Use `.github/workflows/ios-testflight.yml` for signed distribution uploads to App Store Connect + +See [mobile/docs/iOS_TESTFLIGHT](mobile/docs/iOS_TESTFLIGHT.md) for required secrets and setup. ### Downloading Build Artifacts diff --git a/mobile/docs/iOS_TESTFLIGHT.md b/mobile/docs/iOS_TESTFLIGHT.md new file mode 100644 index 000000000..cf09c6de1 --- /dev/null +++ b/mobile/docs/iOS_TESTFLIGHT.md @@ -0,0 +1,47 @@ +# iOS TestFlight GitHub Action + +This repository now includes `.github/workflows/ios-testflight.yml`, which builds a **signed** iOS IPA and uploads it to TestFlight. + +## What must be in the repo + +- The workflow file itself: `.github/workflows/ios-testflight.yml` +- Existing app signing identifiers in Xcode: + - Bundle ID `am.sure.mobile` (or your custom value in the workflow) +- Flutter assets and source already in `mobile/` + +## Required GitHub Secrets + +Set these in **Settings → Secrets and variables → Actions**: + +- `APP_STORE_CONNECT_API_KEY_ID` +- `APP_STORE_CONNECT_API_ISSUER_ID` +- `APP_STORE_CONNECT_API_KEY_BASE64` + - Base64 version of the `.p8` private key content +- `IOS_TEAM_ID` +- `IOS_KEYCHAIN_PASSWORD` +- `IOS_DISTRIBUTION_P12_BASE64` +- `IOS_DISTRIBUTION_P12_PASSWORD` +- `IOS_DISTRIBUTION_CERT_NAME` + - Usually `iPhone Distribution` +- `IOS_PROVISIONING_PROFILE_NAME` +- `IOS_PROVISIONING_PROFILE_BASE64` + +> Do **not** commit private keys, `.p12` files, or `.p8` files to the repository. + +## Triggering + +- Run manually: **Actions → iOS TestFlight → Run workflow** +- Push a tag that matches `ios-v*` (for example `ios-v1.2.3`) + +## Recommended App Store Connect setup + +- Use an API Key with App Manager or Developer role +- Create/download an iOS Distribution certificate (`.p12`), convert to base64 for `IOS_DISTRIBUTION_P12_BASE64` +- Create and distribute an **App Store** provisioning profile for `am.sure.mobile` +- Base64-encode the `.mobileprovision` file for `IOS_PROVISIONING_PROFILE_BASE64` + +## Output + +- IPA artifact: `ios-ipa-testflight` +- Uploaded IPA to TestFlight on successful upload step + diff --git a/mobile/ios/Flutter/Debug.xcconfig b/mobile/ios/Flutter/Debug.xcconfig index ec97fc6f3..e8efba114 100644 --- a/mobile/ios/Flutter/Debug.xcconfig +++ b/mobile/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Profile.xcconfig b/mobile/ios/Flutter/Profile.xcconfig new file mode 100644 index 000000000..3f74b129e --- /dev/null +++ b/mobile/ios/Flutter/Profile.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" +#include "Generated.xcconfig" diff --git a/mobile/ios/Flutter/Release.xcconfig b/mobile/ios/Flutter/Release.xcconfig index c4855bfe2..399e9340e 100644 --- a/mobile/ios/Flutter/Release.xcconfig +++ b/mobile/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 371b2f79a..e91b88fd2 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -68,7 +69,6 @@ FE30D351EF409EF61606DE07 /* Pods-Runner.release.xcconfig */, 1DC166D4710E23F2EDEDA52D /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -86,6 +86,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; @@ -357,21 +358,28 @@ }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 8D7D5C9B2EAF8B6A00F0A1B3 /* Profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = C8WT3YC922; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -494,17 +502,24 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = C8WT3YC922; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -516,16 +531,23 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = C8WT3YC922; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Sure Finances"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = am.sure.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release;