From 1098a1a0177916495299592fcc4e10bdfae33d1f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 13:58:25 -0400 Subject: [PATCH 1/5] Skip Lightning username onboarding + restore navbar inactive icon contrast Two fixes: 1) lib/src/screens/seed/seed_verification/seed_verification_success_view.dart After verifying a BTC wallet's seed, the original flow pushed the user to the lightning_username_page which suggests an @cake.cash Lightning address. We disabled Lightning entirely (no Greenlight infra) and the page is meaningless without it. Collapse the conditional so the button always pops back to the wallet list. Restore the conditional if/when Hash Wallet ever runs its own Lightning backend. 2) lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart inactiveColor was set to theme.colorScheme.primary, which is our brand dark forest green (#1A5C38) post-retheme. That renders nearly invisible against the navbar's semi-transparent dark surface backdrop, so the three non-active tab icons (Wallets / Contacts / Apps) disappear and the user just sees three blob shapes. Material's onSurfaceVariant is the standard token for de-emphasized icons and is visible across our light/dark/black themes. --- .../dashboard/widgets/new_main_navbar_widget.dart | 6 +++++- .../seed_verification_success_view.dart | 14 ++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart index 23f155b6..378f679c 100644 --- a/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart +++ b/lib/src/screens/dashboard/widgets/new_main_navbar_widget.dart @@ -122,7 +122,11 @@ class _NEWNewMainNavBarState extends State { final backgroundColor = theme.colorScheme.surfaceContainer.withAlpha(127); final pillColor = theme.colorScheme.onSurface.withAlpha(25); final activeColor = theme.colorScheme.onSurface; - final inactiveColor = theme.colorScheme.primary; + // Hash Wallet: was theme.colorScheme.primary — that's our brand forest + // green (#1A5C38), which renders dark-on-dark against the navbar's + // semi-transparent surface backdrop. Material's onSurfaceVariant is the + // standard token for de-emphasized icons and is visible across themes. + final inactiveColor = theme.colorScheme.onSurfaceVariant; return Observer( builder: (_) { diff --git a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart index b2409309..1e19e623 100644 --- a/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart +++ b/lib/src/screens/seed/seed_verification/seed_verification_success_view.dart @@ -66,13 +66,15 @@ class SeedVerificationSuccessView extends StatelessWidget { PrimaryButton( key: ValueKey('wallet_seed_page_open_wallet_button_key'), onPressed: () { - if (walletType == WalletType.bitcoin) { - Navigator.of(context).pushNamed(Routes.lightningUsernamePage, arguments: true); - } else { - Navigator.of(context).popUntil((route) => route.isFirst); - } + // Hash Wallet: Lightning Username onboarding skipped. The original + // Cake flow pushed BTC wallets to the lightning_username_page, + // which suggests an @cake.cash address. We disabled Lightning + // entirely (no Greenlight infra) and the screen is meaningless + // without it. Re-enable here if we ever stand up our own LN + // backend + domain. + Navigator.of(context).popUntil((route) => route.isFirst); }, - text: (walletType == WalletType.bitcoin) ? S.current.continue_text : S.current.open_wallet, + text: S.current.open_wallet, color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, ), -- 2.50.1 (Apple Git-155) From 360556b8f79bbdb3aadcf40d6877099a9eeae53f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 14:12:36 -0400 Subject: [PATCH 2/5] Top-bar: use onSurfaceVariant for the gear icon (same fix as navbar) Same root cause as the bottom navbar invisibility: iconColor was set to theme.colorScheme.primary, which on the light theme is our dark forest green (#1A5C38) and goes invisible on dark surfaces. onSurfaceVariant is Material's de-emphasized-foreground token and has guaranteed contrast against the surface across all our themes. --- lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart index e3e28871..78e7353d 100644 --- a/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart +++ b/lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart @@ -45,7 +45,11 @@ class TopBar extends StatelessWidget { isSyncHeavy: dashboardViewModel.isSyncHeavy, ), ModernButton.svg( - iconColor: Theme.of(context).colorScheme.primary, + // Hash Wallet: was theme.colorScheme.primary (dark forest green) + // which rendered the gear icon nearly invisible on the dark + // dashboard backdrop. onSurfaceVariant is the de-emphasized- + // foreground Material token. + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, size: 36, onPressed: () { HapticFeedback.mediumImpact(); -- 2.50.1 (Apple Git-155) From 083f8b7f2adfde011dd880a39929aae09ed05695 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 20:48:44 -0400 Subject: [PATCH 3/5] CI: add Android build workflow (signed APK + AAB) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the Linux workflow's approach — uses the Cake-Labs prebuilt Docker image (Flutter 3.32.0 + NDK r28 + Go + Android SDK + JDK17 + all native build deps) and the prebuilt monero_c .so bundle, so we skip the multi-hour native cross-compile. Stages monero + wownero .so files into android/app/src/main/jniLibs/ for all 4 Android ABIs (arm64-v8a, armeabi-v7a, x86, x86_64). Zano libs in the bundle are ignored. Decodes the upload keystore from secret ANDROID_KEYSTORE_BASE64, writes android/key.properties (which build.gradle's signingConfigs already reads), builds a universal release APK for sideload + an AAB for Play Console. Both signed with the upload key; Play App Signing re-signs the AAB server-side with the production key on upload. Required Gitea Actions secrets: ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD TROCADOR_API_KEY (already set, reused from Linux) TROCADOR_MONERO_API_KEY (same) TROCADOR_EXCHANGE_MARKUP (same) mwebd (Litecoin MWEB privacy) is NOT built. LTC works without it, just no MWEB feature. Add the build step + go build pipeline if we want MWEB on Android — separate effort. Artifacts: - hash-wallet-android-apk- (single universal APK, 14-day retention) - hash-wallet-android-aab- (Play Console upload, 30-day retention) --- .github/workflows/build-android.yml | 228 ++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 .github/workflows/build-android.yml diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 00000000..ffbca15c --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,228 @@ +name: Hash Wallet Android build + +# Triggers: +# - push to dev/main (validate every commit) +# - PRs targeting dev/main (gate merges) +# - manual via workflow_dispatch +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: ubuntu-latest + container: + # Same Cake-Labs prebuilt image used for Linux. Bundles Flutter 3.32.0, + # Android NDK r28, Go 1.24.1, Android SDK, JDK 17, gradle, and the + # native build deps (boost, openssl, libsodium, libzmq, unbound, icu). + image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.32.0-ndkr28-go1.24.1-ruststablenightly + env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + APP_ANDROID_TYPE: cakewallet + + steps: + - name: Fix Actions HOME (writeable for ~/.cache + gradle) + run: echo "HOME=/root" >> $GITHUB_ENV + + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Configure git inside the container + run: | + git config --global --add safe.directory '*' + git config --global user.email "ci@suchsoftware.com" + git config --global user.name "Hash Wallet CI" + + # ---- External prebuilt deps (same approach as Linux workflow) -------- + - name: Fetch prebuilt torch_dart + run: | + set -x -e + pushd scripts + rm -rf torch_dart torch_dart.tar.gz + wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz + mkdir torch_dart + tar -xzf torch_dart.tar.gz -C torch_dart + rm torch_dart.tar.gz + popd + + - name: Fetch prebuilt reown_flutter + run: | + set -x -e + pushd scripts + rm -rf reown_flutter reown_flutter.tar.gz + wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz + mkdir reown_flutter + tar -xzf reown_flutter.tar.gz -C reown_flutter + rm reown_flutter.tar.gz + popd + + - name: Clone BitBox Flutter + run: | + set -x -e + pushd scripts + ./build_bitbox_flutter.sh + popd + + # ---- Native crypto cores (monero_c prebuilt bundle) ------------------ + # Same release-bundle.zip as Linux. Contains pre-cross-compiled .so + # files for monero/wownero/zano across all 4 Android ABIs. We only ship + # monero + wownero — zano libs in the bundle are ignored. + - name: Fetch prebuilt monero_c .so bundle + run: | + set -x -e + ./scripts/prepare_moneroc.sh + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + echo "monero_c TAG: $MONERO_C_TAG" + mkdir -p "scripts/monero_c/release/$MONERO_C_TAG" + pushd "scripts/monero_c/release/$MONERO_C_TAG" + wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip + unzip -q release-bundle.zip + rm release-bundle.zip + ls + popd + + # ---- Stage native libs into android/app/src/main/jniLibs// ----- + # Flutter Android packaging auto-includes everything under jniLibs. + - name: Place Android .so files into jniLibs + run: | + set -x -e + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG" + + # monero_c target dir → Android ABI dir + declare -A ABI_MAP=( + [aarch64-linux-android]=arm64-v8a + [armv7a-linux-androideabi]=armeabi-v7a + [i686-linux-android]=x86 + [x86_64-linux-android]=x86_64 + ) + + for target in "${!ABI_MAP[@]}"; do + abi="${ABI_MAP[$target]}" + mkdir -p "android/app/src/main/jniLibs/$abi" + for coin in monero wownero; do + src="$BUNDLE_DIR/$target/lib${coin}_wallet2_api_c.so" + if [[ -f "$src" ]]; then + cp -v "$src" "android/app/src/main/jniLibs/$abi/" + else + echo "MISSING: $src — fail loud, the bundle layout may have changed" + exit 1 + fi + done + done + + echo "=== jniLibs contents ===" + find android/app/src/main/jniLibs/ -type f -name '*.so' | sort + + # mwebd (Litecoin MWEB) is NOT built here. LTC works without MWEB — + # users just don't get the privacy MWEB feature. Add a build_mwebd + # step if/when we want MWEB on Android. + + # ---- Configure: pubspec.yaml, AndroidManifest.xml, app_properties ---- + - name: Run Android configure (cakewallet profile) + run: | + set -x -e + pushd scripts/android + source ./app_env.sh cakewallet + ./app_config.sh + popd + + # ---- Secrets --------------------------------------------------------- + - name: Generate per-module secrets.g.dart files (all empty defaults) + run: dart run tool/generate_new_secrets.dart + + - name: Inject Trocador affiliate secrets into lib/.secrets.g.dart + env: + TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} + TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }} + TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }} + run: | + sed -i \ + -e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \ + -e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \ + -e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \ + lib/.secrets.g.dart + grep '^const trocador' lib/.secrets.g.dart + + # ---- Signing: decode upload keystore + write key.properties --------- + - name: Decode Android upload keystore + write key.properties + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + set -e + if [[ -z "$ANDROID_KEYSTORE_BASE64" ]]; then + echo "FATAL: ANDROID_KEYSTORE_BASE64 not set — configure Gitea Actions secrets first" + exit 1 + fi + # Write decoded keystore next to build.gradle (storeFile path + # in key.properties is resolved relative to android/app/). + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks + ls -la android/app/upload-keystore.jks + # key.properties consumed by build.gradle's signingConfigs.release. + cat > android/key.properties </dev/null | sort -V | tail -1 || which apksigner) + if [[ -n "$APKSIGNER" ]]; then + "$APKSIGNER" verify --print-certs build/app/outputs/flutter-apk/app-release.apk || true + else + echo "apksigner not on PATH; skipping sig print" + fi + + # actions/upload-artifact@v4 doesn't work on Gitea; pin to v3. + - name: Upload APK artifact (sideload-able) + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-android-apk-${{ github.sha }} + path: build/app/outputs/flutter-apk/app-release.apk + retention-days: 14 + + - name: Upload AAB artifact (for Play Console) + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-android-aab-${{ github.sha }} + path: build/app/outputs/bundle/release/app-release.aab + retention-days: 30 -- 2.50.1 (Apple Git-155) From 6304521e92ed860ebccddf3f894a30d4078deaa7 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 21:22:07 -0400 Subject: [PATCH 4/5] CI: iOS Simulator build workflow (no signing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of iOS CI. Validates the Mac runner end-to-end with the simplest possible target: an unsigned simulator .app. Runs on the mac-mini-m2 self-hosted runner (label: macos-latest). Mirrors the Linux/Android workflows for the shared prep steps (torch_dart, reown_flutter, bitbox_flutter, monero_c prebuilt bundle, configure, codegen, Trocador secrets), then does flutter build ios --simulator --no-codesign and zips Runner.app. Artifact: hash-wallet-ios-sim-. Unzip on any Apple Silicon Mac, drag Runner.app into Simulator to run. Phase 2 (TestFlight, separate file) follows after Phase 1 is green — adds keychain setup, .p12 + provisioning profile install, ipa build, xcrun altool upload using the App Store Connect API key. All Apple secrets are already configured in Gitea. The 'Inspect iOS targets in monero_c bundle' debug step will reveal which Apple targets the prebuilt bundle ships — we'll need aarch64-apple-ios-sim (or similar) for arm64 simulator builds on M-series Macs. If missing, follow-up will either fetch from a different source or build the iOS .a archives from source via Cake's scripts/ios/ build_*.sh chain (slow but reliable). --- .github/workflows/build-ios-sim.yml | 164 ++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/build-ios-sim.yml diff --git a/.github/workflows/build-ios-sim.yml b/.github/workflows/build-ios-sim.yml new file mode 100644 index 00000000..31db21bb --- /dev/null +++ b/.github/workflows/build-ios-sim.yml @@ -0,0 +1,164 @@ +name: Hash Wallet iOS Simulator build + +# Phase 1 of iOS CI: validate the Mac runner end-to-end by producing an +# unsigned simulator .app. Drop the resulting Runner.app into the iOS +# Simulator on any Apple Silicon Mac to verify the app actually compiles +# and launches. +# +# Phase 2 (separate workflow): full TestFlight pipeline with signing. + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: macos-latest + env: + APP_IOS_TYPE: cakewallet + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Show toolchain + run: | + set -x + flutter --version + xcodebuild -version + pod --version + which wget unzip + uname -m + + # ---- External prebuilt deps (same as android/linux) ------------------ + - name: Fetch prebuilt torch_dart + run: | + set -x -e + pushd scripts + rm -rf torch_dart torch_dart.tar.gz + wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz + mkdir torch_dart + tar -xzf torch_dart.tar.gz -C torch_dart + rm torch_dart.tar.gz + popd + + - name: Fetch prebuilt reown_flutter + run: | + set -x -e + pushd scripts + rm -rf reown_flutter reown_flutter.tar.gz + wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz + mkdir reown_flutter + tar -xzf reown_flutter.tar.gz -C reown_flutter + rm reown_flutter.tar.gz + popd + + - name: Clone BitBox Flutter + run: | + set -x -e + pushd scripts + ./build_bitbox_flutter.sh + popd + + # ---- Native crypto cores (monero_c prebuilt bundle) ------------------ + - name: Fetch prebuilt monero_c bundle + run: | + set -x -e + ./scripts/prepare_moneroc.sh + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + mkdir -p "scripts/monero_c/release/$MONERO_C_TAG" + pushd "scripts/monero_c/release/$MONERO_C_TAG" + wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip + unzip -q release-bundle.zip + rm release-bundle.zip + echo "=== bundle contents (one level) ===" + ls + popd + + # iOS native lib staging: Cake's scripts/ios/setup.sh + gen_framework.sh + # expect pre-built .a archives in specific paths. The first run will + # likely surface what's missing; we iterate from there. + - name: Inspect iOS targets available in monero_c bundle + run: | + set -x + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG" + # List anything iOS-y + find "$BUNDLE_DIR" -maxdepth 1 -name '*ios*' -o -name '*apple*' 2>/dev/null || true + ls "$BUNDLE_DIR" || true + + # ---- Configure: pubspec.yaml, Info.plist, GeneratedPluginRegistrant --- + - name: Run iOS configure (cakewallet profile) + run: | + set -x -e + pushd scripts/ios + source ./app_env.sh cakewallet + ./app_config.sh + popd + + # ---- Secrets --------------------------------------------------------- + - name: Generate per-module secrets.g.dart files (all empty defaults) + run: dart run tool/generate_new_secrets.dart + + - name: Inject Trocador affiliate secrets + env: + TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} + TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }} + TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }} + run: | + # macOS sed needs '' after -i; Linux sed doesn't accept that. This + # workflow runs on macos only. + sed -i '' \ + -e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \ + -e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \ + -e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \ + lib/.secrets.g.dart + grep '^const trocador' lib/.secrets.g.dart + + # ---- Flutter init + codegen ----------------------------------------- + - name: Initialize Flutter SDK (iOS precache) + run: | + flutter --version + flutter precache --ios --no-linux --no-android --no-macos --no-windows --no-fuchsia --no-web || true + + - name: Build generated code (mobx + hive adapters) + run: bash model_generator.sh + + - name: Generate localization + run: dart run tool/generate_localization.dart + + # CocoaPods: the iOS Podfile installs Flutter plugin pods. Required + # before flutter build can link them. + - name: pod install + run: | + cd ios + pod install --repo-update + + # ---- Build iOS Simulator app (no codesign) --------------------------- + - name: Build iOS Simulator app + run: | + set -x -e + flutter build ios --simulator --no-codesign \ + --dart-define-from-file=env.json + ls -la build/ios/iphonesimulator/ + + - name: Zip Runner.app + run: | + cd build/ios/iphonesimulator + zip -r hash_wallet_ios_sim_${{ github.sha }}.zip Runner.app + + - name: Upload .app artifact + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-ios-sim-${{ github.sha }} + path: build/ios/iphonesimulator/hash_wallet_ios_sim_*.zip + retention-days: 14 -- 2.50.1 (Apple Git-155) From c7f64f8273dd7aa0a17e64ddfa78cedf58ce979c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 18 May 2026 21:23:54 -0400 Subject: [PATCH 5/5] CI: iOS TestFlight build workflow (signed + upload via altool) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of iOS CI. Full signed pipeline that: 1) Decodes secrets: - APPLE_DIST_CERT_P12_BASE64 + password → temp keychain - APPLE_PROVISIONING_PROFILE_BASE64 → ~/Library/MobileDevice/... - APPLE_API_KEY_P8_BASE64 → ~/.appstoreconnect/private_keys/ 2) Parses the .mobileprovision for its UUID + Name (used in ExportOptions). 3) Generates ios/ExportOptions.plist dynamically with the right Team ID + profile name + 'app-store' method. 4) Runs flutter build ipa --release --export-options-plist=... 5) Uploads the .ipa to TestFlight via xcrun altool with the API key. 6) Always cleans up keychain + temp credentials, even on failure. Trigger: workflow_dispatch ONLY (manual). TestFlight uploads consume a build number with every push — we don't want auto-trigger spamming TestFlight. Bump the build number in scripts/ios/app_env.sh before each TestFlight push (or wire a workflow_dispatch input later). All Apple secrets (and the Trocador ones reused from Linux/Android) are already configured in Gitea Actions. If signing fails on first run: most likely the profile name extracted from the .mobileprovision doesn't match what Xcode expects, OR the distribution cert in the .p12 doesn't match what the profile pins. The keychain identity sanity-check + ExportOptions.plist echo in the logs will tell us which. --- .github/workflows/build-ios-testflight.yml | 266 +++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 .github/workflows/build-ios-testflight.yml diff --git a/.github/workflows/build-ios-testflight.yml b/.github/workflows/build-ios-testflight.yml new file mode 100644 index 00000000..40151b56 --- /dev/null +++ b/.github/workflows/build-ios-testflight.yml @@ -0,0 +1,266 @@ +name: Hash Wallet iOS TestFlight build + +# Phase 2 of iOS CI: full signed build that uploads to TestFlight. +# Phase 1 (build-ios-sim.yml) validates the Mac runner with no signing — +# run that first if TestFlight blows up unexpectedly. +# +# Required Gitea Actions secrets (all account/app credentials): +# APPLE_TEAM_ID — 10-char (D8PL9F7X33) +# APPLE_KEY_ID — 10-char (CHK6B69G58) +# APPLE_ISSUER_ID — UUID +# APPLE_API_KEY_P8_BASE64 — base64 of AuthKey_.p8 +# APPLE_DIST_CERT_P12_BASE64 — base64 of distribution.p12 +# APPLE_DIST_CERT_P12_PASSWORD — password set during openssl pkcs12 export +# APPLE_PROVISIONING_PROFILE_BASE64 — base64 of Hash_Wallet.mobileprovision +# APPLE_KEYCHAIN_PASSWORD — any string, used to create the temp keychain +# TROCADOR_* — reused from Linux/Android + +on: + # Don't auto-trigger on every push — TestFlight uploads are slow and use + # build numbers. Manual trigger only; later we can add a workflow_dispatch + # input for build number or auto-bump. + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build: + runs-on: macos-latest + env: + APP_IOS_TYPE: cakewallet + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Show toolchain + run: | + set -x + flutter --version + xcodebuild -version + pod --version + uname -m + + # ---- External prebuilt deps ----------------------------------------- + - name: Fetch prebuilt torch_dart + run: | + set -x -e + pushd scripts + rm -rf torch_dart torch_dart.tar.gz + wget -q https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz + mkdir torch_dart + tar -xzf torch_dart.tar.gz -C torch_dart + rm torch_dart.tar.gz + popd + + - name: Fetch prebuilt reown_flutter + run: | + set -x -e + pushd scripts + rm -rf reown_flutter reown_flutter.tar.gz + wget -q https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz + mkdir reown_flutter + tar -xzf reown_flutter.tar.gz -C reown_flutter + rm reown_flutter.tar.gz + popd + + - name: Clone BitBox Flutter + run: | + set -x -e + pushd scripts + ./build_bitbox_flutter.sh + popd + + - name: Fetch prebuilt monero_c bundle + run: | + set -x -e + ./scripts/prepare_moneroc.sh + MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) + mkdir -p "scripts/monero_c/release/$MONERO_C_TAG" + pushd "scripts/monero_c/release/$MONERO_C_TAG" + wget -q https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip + unzip -q release-bundle.zip + rm release-bundle.zip + popd + + # ---- Configure: pubspec.yaml, Info.plist, etc. ---------------------- + - name: Run iOS configure (cakewallet profile) + run: | + set -x -e + pushd scripts/ios + source ./app_env.sh cakewallet + ./app_config.sh + popd + + # ---- Secrets --------------------------------------------------------- + - name: Generate per-module secrets.g.dart files (empty defaults) + run: dart run tool/generate_new_secrets.dart + + - name: Inject Trocador affiliate secrets + env: + TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} + TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }} + TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }} + run: | + sed -i '' \ + -e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \ + -e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \ + -e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \ + lib/.secrets.g.dart + + # ---- Apple signing setup -------------------------------------------- + - name: Create temp keychain + import distribution cert + env: + APPLE_DIST_CERT_P12_BASE64: ${{ secrets.APPLE_DIST_CERT_P12_BASE64 }} + APPLE_DIST_CERT_P12_PASSWORD: ${{ secrets.APPLE_DIST_CERT_P12_PASSWORD }} + APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + run: | + set -e + KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" + # Create + unlock dedicated keychain so we don't touch the user's login keychain + security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" # 6 hours + security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + # Add to user search list so codesign can find it + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g') + # Import the .p12 + P12="$RUNNER_TEMP/dist.p12" + echo "$APPLE_DIST_CERT_P12_BASE64" | base64 -d > "$P12" + security import "$P12" -k "$KEYCHAIN_PATH" -P "$APPLE_DIST_CERT_P12_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + # Sanity-check: list identities the keychain offers + security find-identity -v -p codesigning "$KEYCHAIN_PATH" + rm -f "$P12" + + - name: Install provisioning profile + parse name/UUID for ExportOptions + env: + APPLE_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_PROVISIONING_PROFILE_BASE64 }} + run: | + set -e + PROFILES_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" + mkdir -p "$PROFILES_DIR" + PROFILE="$RUNNER_TEMP/Hash_Wallet.mobileprovision" + echo "$APPLE_PROVISIONING_PROFILE_BASE64" | base64 -d > "$PROFILE" + + # Parse the .mobileprovision to get its UUID + Name (CMS-decoded plist). + PROFILE_UUID=$(security cms -D -i "$PROFILE" | plutil -extract UUID raw -o - -) + PROFILE_NAME=$(security cms -D -i "$PROFILE" | plutil -extract Name raw -o - -) + echo "Profile UUID: $PROFILE_UUID" + echo "Profile name: $PROFILE_NAME" + echo "PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV" + echo "PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV" + + # Xcode looks profiles up by UUID filename. + cp "$PROFILE" "$PROFILES_DIR/$PROFILE_UUID.mobileprovision" + ls -la "$PROFILES_DIR" | head -10 + + - name: Install App Store Connect API key for altool + env: + APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }} + APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + run: | + set -e + mkdir -p "$HOME/.appstoreconnect/private_keys" + echo "$APPLE_API_KEY_P8_BASE64" | base64 -d > "$HOME/.appstoreconnect/private_keys/AuthKey_${APPLE_KEY_ID}.p8" + ls -la "$HOME/.appstoreconnect/private_keys/" + + # ---- Flutter init + codegen + pod install --------------------------- + - name: Initialize Flutter SDK (iOS precache) + run: | + flutter --version + flutter precache --ios --no-linux --no-android --no-macos --no-windows --no-fuchsia --no-web || true + + - name: Build generated code + run: bash model_generator.sh + + - name: Generate localization + run: dart run tool/generate_localization.dart + + - name: pod install + run: | + cd ios + pod install --repo-update + + # ---- Generate ExportOptions.plist + build IPA ----------------------- + - name: Write ExportOptions.plist + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + cat > ios/ExportOptions.plist < + + + + method + app-store + teamID + ${APPLE_TEAM_ID} + signingStyle + manual + signingCertificate + Apple Distribution + provisioningProfiles + + com.suchsoftware.hashwallet + ${PROFILE_NAME} + + uploadBitcode + + uploadSymbols + + destination + export + stripSwiftSymbols + + + + EOF + cat ios/ExportOptions.plist + + - name: Build IPA + run: | + set -x -e + flutter build ipa --release \ + --dart-define-from-file=env.json \ + --export-options-plist=ios/ExportOptions.plist + ls -la build/ios/ipa/ + + - name: Upload IPA artifact + uses: actions/upload-artifact@v3 + with: + name: hash-wallet-ios-${{ github.sha }} + path: build/ios/ipa/*.ipa + retention-days: 30 + + # ---- Upload to TestFlight ------------------------------------------- + - name: Upload to TestFlight via altool + env: + APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} + run: | + set -e + IPA=$(ls build/ios/ipa/*.ipa | head -1) + echo "Uploading $IPA to TestFlight..." + xcrun altool --upload-app \ + --type ios \ + --file "$IPA" \ + --apiKey "$APPLE_KEY_ID" \ + --apiIssuer "$APPLE_ISSUER_ID" + + # ---- Cleanup keychain so we don't leak it across builds ------------- + - name: Cleanup + if: always() + env: + APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} + run: | + set +e + KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" + security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null + rm -rf "$HOME/.appstoreconnect/private_keys" + rm -f "$HOME/Library/MobileDevice/Provisioning Profiles"/*.mobileprovision + true -- 2.50.1 (Apple Git-155)