diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 4a8b2b421a..f5ba1b76ff 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -229,10 +229,19 @@ jobs: MEAWALLET_NEXUS_PASSWORD: ${{ secrets.MEAWALLET_NEXUS_PASSWORD_ANDROID }} ANDROID_VERSION_NAME: ${{ steps.version.outputs.name }} run: | - # Monotonic (run_number always increases). +10000 clears legacy - # codes already on Play from earlier manual/local uploads (small - # console codes plus git-commit-count builds up to ~8600). - export ANDROID_VERSION_CODE=$((10000 + GITHUB_RUN_NUMBER)) + # versionCode must increase in UPLOAD order, which is not run + # order: GitHub preserves run_number when you re-run an older + # workflow and only bumps run_attempt, so any run-numbered or + # version-derived code goes backwards when dispatch 1 is + # re-run after dispatch 2 has already uploaded — and Play + # refuses the recovery upload, leaving the retry wedged. + # scripts/android-version-code.mjs uses wall-clock seconds, + # the only value monotonic by construction rather than by an + # argument about GitHub's numbering. See the header there for + # the two schemes this replaces. + ANDROID_VERSION_CODE="$(node scripts/android-version-code.mjs)" + export ANDROID_VERSION_CODE + echo "versionCode $ANDROID_VERSION_CODE (version $ANDROID_VERSION_NAME, run $GITHUB_RUN_NUMBER attempt $GITHUB_RUN_ATTEMPT)" pnpm native:release - name: Upload to Google Play diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml index 239f66ba17..4a3acd117a 100644 --- a/.github/workflows/ios-release.yml +++ b/.github/workflows/ios-release.yml @@ -197,15 +197,6 @@ jobs: - name: Build web + sync iOS env: - # MeaWallet MPP SDK (Apple Pay push provisioning): postsync - # vendors the xcframework only when these are set, so the - # build stays green until the secrets are provisioned. - # MeaWallet issues separate iOS and Android Nexus credentials, - # and an Environment secret holds one value per name — sharing - # a name would send whichever pair was configured last to both - # platforms and fail the other one's SDK download. - MEAWALLET_NEXUS_USER: ${{ secrets.MEAWALLET_NEXUS_USER_IOS }} - MEAWALLET_NEXUS_PASSWORD: ${{ secrets.MEAWALLET_NEXUS_PASSWORD_IOS }} # postsync stamps project.pbxproj; without this it would stamp # package.json's version, which no longer tracks releases. IOS_MARKETING_VERSION: ${{ steps.version.outputs.name }} @@ -213,83 +204,39 @@ jobs: node scripts/native-build.js && npx cap sync ios if [ -f scripts/native-ios-postsync.js ]; then node scripts/native-ios-postsync.js; fi - - name: Write MeaWallet config - # mea_config (encrypted, non-sensitive per MeaWallet, but kept out - # of git) — the Copy MeaWallet Config build phase bundles it when - # present; no-op until the secret is provisioned. - env: - MEA_CONFIG: ${{ secrets.MEAWALLET_CONFIG_BASE64 }} - run: | - if [ -n "$MEA_CONFIG" ]; then - echo "$MEA_CONFIG" | base64 -d > ios/App/App/mea_config - echo "mea_config written" - else - echo "MEAWALLET_CONFIG_BASE64 not set — push provisioning stays stubbed" - fi - - name: Install Apple distribution certificate uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7 with: p12-file-base64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }} p12-password: ${{ secrets.IOS_DIST_CERT_PASSWORD }} - - name: Install provisioning profiles + - name: Install provisioning profile id: provision env: IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }} - IOS_PROVISIONING_PROFILE_EXT_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_EXT_BASE64 }} - IOS_PROVISIONING_PROFILE_EXT_UI_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_EXT_UI_BASE64 }} run: | PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" mkdir -p "$PROFILE_DIR" - - # The archive is manual-signed, so EVERY bundle it embeds needs its - # own profile installed and named in ExportOptions — a missing - # extension profile fails the archive, not just the extension. - install_profile() { - local b64="$1" out="$2" var="$3" - if [ -z "$b64" ]; then - echo "::error::$var is empty — the app and both Wallet extensions each need their profile" - exit 1 - fi - echo "$b64" | base64 -d > "/tmp/$out.mobileprovision" - # Decode the profile to read its UUID + Name (no extra secret needed). - security cms -D -i "/tmp/$out.mobileprovision" > "/tmp/$out.plist" - local uuid name - uuid=$(/usr/libexec/PlistBuddy -c 'Print :UUID' "/tmp/$out.plist") - name=$(/usr/libexec/PlistBuddy -c 'Print :Name' "/tmp/$out.plist") - cp "/tmp/$out.mobileprovision" "$PROFILE_DIR/$uuid.mobileprovision" - echo "$out=$name" >> "$GITHUB_OUTPUT" - echo "Installed provisioning profile: $name ($uuid)" - } - - install_profile "$IOS_PROVISIONING_PROFILE_BASE64" profile IOS_PROVISIONING_PROFILE_BASE64 - install_profile "$IOS_PROVISIONING_PROFILE_EXT_BASE64" ext IOS_PROVISIONING_PROFILE_EXT_BASE64 - install_profile "$IOS_PROVISIONING_PROFILE_EXT_UI_BASE64" extui IOS_PROVISIONING_PROFILE_EXT_UI_BASE64 - + echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -d > /tmp/profile.mobileprovision + # Decode the profile to read its UUID + Name (no extra secret needed). + security cms -D -i /tmp/profile.mobileprovision > /tmp/profile.plist + PROFILE_UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /tmp/profile.plist) + PROFILE_NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' /tmp/profile.plist) + cp /tmp/profile.mobileprovision "$PROFILE_DIR/$PROFILE_UUID.mobileprovision" + echo "name=$PROFILE_NAME" >> "$GITHUB_OUTPUT" + echo "Installed provisioning profile: $PROFILE_NAME ($PROFILE_UUID)" # A profile without aps-environment ships an app that can never # register with APNs, even when the entitlements file asks for it. - # Only the app carries push; the extensions legitimately do not. PROFILE_APS=$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:aps-environment' /tmp/profile.plist 2>/dev/null || true) if [ "$PROFILE_APS" != "production" ]; then echo "::error::provisioning profile aps-environment is '${PROFILE_APS:-missing}' — enable Push Notifications on the App ID, regenerate the profile, and update IOS_PROVISIONING_PROFILE_BASE64" exit 1 fi - # The app and the extensions share group.me.peanut.wallet; if the app's - # profile predates that capability the archive signs but the extension - # can never read the store the app writes. - if ! /usr/libexec/PlistBuddy -c 'Print :Entitlements:com.apple.security.application-groups' /tmp/profile.plist 2>/dev/null | grep -q 'group.me.peanut.wallet$'; then - echo "::error::the app profile is missing the group.me.peanut.wallet app group — regenerate it after enabling the App Group on me.peanut.wallet and update IOS_PROVISIONING_PROFILE_BASE64" - exit 1 - fi - - name: Archive & export IPA env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - PROFILE_NAME: ${{ steps.provision.outputs.profile }} - PROFILE_NAME_EXT: ${{ steps.provision.outputs.ext }} - PROFILE_NAME_EXT_UI: ${{ steps.provision.outputs.extui }} + PROFILE_NAME: ${{ steps.provision.outputs.name }} # Monotonic, always-increments-even-on-rerun. TestFlight requires each # upload's build number to exceed the last. IOS_BUILD_NUMBER: ${{ github.run_number }} @@ -325,10 +272,6 @@ jobs: me.peanut.wallet ${PROFILE_NAME} - me.peanut.wallet.PushProvisioningExtension - ${PROFILE_NAME_EXT} - me.peanut.wallet.PushProvisioningExtensionUI - ${PROFILE_NAME_EXT_UI} uploadSymbols diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 1099aeefee..0e6ced2ff6 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -7,67 +7,32 @@ objects = { /* Begin PBXBuildFile section */ - 117DAC66C212FB8CE1D60CCB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51485C04EED71F615CEB1B1D /* Foundation.framework */; }; - 280163DBDEA95EE6C90C5450 /* WalletExtensionCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25F9757EB14D1213BE3F218 /* WalletExtensionCardStore.swift */; }; 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; - 488EE5998B1CA8D44D7F3204 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51485C04EED71F615CEB1B1D /* Foundation.framework */; }; 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; }; 5E53A1B2C3D4E5F600000001 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 5E53A1B2C3D4E5F600000002 /* Sentry */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 7A1C0DE7C11B0A4D2E5F3901 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1C0DE7C11B0A4D2E5F3902 /* AppViewController.swift */; }; + 7A1C0DE7C11B0A4D2E5F3903 /* ClipboardDetectPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1C0DE7C11B0A4D2E5F3904 /* ClipboardDetectPlugin.swift */; }; 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; - 7A1C0DE7C11B0A4D2E5F3901 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1C0DE7C11B0A4D2E5F3902 /* AppViewController.swift */; }; - 7A1C0DE7C11B0A4D2E5F3903 /* ClipboardDetectPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1C0DE7C11B0A4D2E5F3904 /* ClipboardDetectPlugin.swift */; }; - 7A1C0DE7C11B0A4D2E5F3915 /* PushProvisioningExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 41A22DB09E56D951D8F76C5C /* PushProvisioningExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 7A1C0DE7C11B0A4D2E5F3916 /* PushProvisioningExtensionUI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1F92515716B856A0CE9DF789 /* PushProvisioningExtensionUI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 7A1C0DE7C11B0A4D2E5F3905 /* PushProvisioningPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1C0DE7C11B0A4D2E5F3906 /* PushProvisioningPlugin.swift */; }; - 7A668B3F8C26297AC5BEC8AE /* WalletExtensionCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D25F9757EB14D1213BE3F218 /* WalletExtensionCardStore.swift */; }; - BF9C96F67827E0FE790DF482 /* IssuerExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64C97DDF5A8819E8D49B613 /* IssuerExtensionHandler.swift */; }; - FE137B7240B844B8CABFD47C /* IssuerAuthorizationExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC1338A051766837F4F534F /* IssuerAuthorizationExtensionHandler.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 7A1C0DE7C11B0A4D2E5F3910 /* Embed App Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 7A1C0DE7C11B0A4D2E5F3915 /* PushProvisioningExtension.appex in Embed App Extensions */, - 7A1C0DE7C11B0A4D2E5F3916 /* PushProvisioningExtensionUI.appex in Embed App Extensions */, - ); - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ - 1DAB657B3E3B0050DB3704AA /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 1F92515716B856A0CE9DF789 /* PushProvisioningExtensionUI.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PushProvisioningExtensionUI.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 29F98636F7E05F181F8BEA43 /* PushProvisioningExtensionUI.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = PushProvisioningExtensionUI.entitlements; sourceTree = ""; }; 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; - 41A22DB09E56D951D8F76C5C /* PushProvisioningExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PushProvisioningExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7A1C0DE7C11B0A4D2E5F3902 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = ""; }; + 7A1C0DE7C11B0A4D2E5F3904 /* ClipboardDetectPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardDetectPlugin.swift; sourceTree = ""; }; 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; - 51485C04EED71F615CEB1B1D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - 7A1C0DE7C11B0A4D2E5F3902 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = ""; }; - 7A1C0DE7C11B0A4D2E5F3904 /* ClipboardDetectPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardDetectPlugin.swift; sourceTree = ""; }; - 7A1C0DE7C11B0A4D2E5F3906 /* PushProvisioningPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushProvisioningPlugin.swift; sourceTree = ""; }; 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; - A64C97DDF5A8819E8D49B613 /* IssuerExtensionHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IssuerExtensionHandler.swift; sourceTree = ""; }; - C74EEB95F977642CDF8DEF29 /* PushProvisioningExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = PushProvisioningExtension.entitlements; sourceTree = ""; }; - D25F9757EB14D1213BE3F218 /* WalletExtensionCardStore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletExtensionCardStore.swift; sourceTree = ""; }; - D4358D54E0FA0205DEF87539 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DBC1338A051766837F4F534F /* IssuerAuthorizationExtensionHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IssuerAuthorizationExtensionHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -80,42 +45,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B35C2FC9E15503BC34FE55D5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 488EE5998B1CA8D44D7F3204 /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EE470AF53AAE4DD2C34C140B /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 117DAC66C212FB8CE1D60CCB /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1A3B48A3C58BDE7F8D5C0F99 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 845EE741AB37330EC7ADD6EB /* iOS */, - ); - name = Frameworks; - sourceTree = ""; - }; 504EC2FB1FED79650016851F = { isa = PBXGroup; children = ( 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, 504EC3061FED79650016851F /* App */, 504EC3051FED79650016851F /* Products */, - 1A3B48A3C58BDE7F8D5C0F99 /* Frameworks */, - FBCFE0300596CE4AF772CF74 /* PushProvisioningExtension */, - AF92BE71BAB4901DCE7C5830 /* PushProvisioningExtensionUI */, ); sourceTree = ""; }; @@ -123,8 +61,6 @@ isa = PBXGroup; children = ( 504EC3041FED79650016851F /* App.app */, - 41A22DB09E56D951D8F76C5C /* PushProvisioningExtension.appex */, - 1F92515716B856A0CE9DF789 /* PushProvisioningExtensionUI.appex */, ); name = Products; sourceTree = ""; @@ -136,85 +72,19 @@ 504EC3071FED79650016851F /* AppDelegate.swift */, 7A1C0DE7C11B0A4D2E5F3902 /* AppViewController.swift */, 7A1C0DE7C11B0A4D2E5F3904 /* ClipboardDetectPlugin.swift */, - 7A1C0DE7C11B0A4D2E5F3906 /* PushProvisioningPlugin.swift */, 504EC30B1FED79650016851F /* Main.storyboard */, 504EC30E1FED79650016851F /* Assets.xcassets */, 504EC3101FED79650016851F /* LaunchScreen.storyboard */, 504EC3131FED79650016851F /* Info.plist */, 2FAD9762203C412B000D30F8 /* config.xml */, 50B271D01FEDC1A000F3C39B /* public */, - D25F9757EB14D1213BE3F218 /* WalletExtensionCardStore.swift */, ); path = App; sourceTree = ""; }; - 845EE741AB37330EC7ADD6EB /* iOS */ = { - isa = PBXGroup; - children = ( - 51485C04EED71F615CEB1B1D /* Foundation.framework */, - ); - name = iOS; - sourceTree = ""; - }; - AF92BE71BAB4901DCE7C5830 /* PushProvisioningExtensionUI */ = { - isa = PBXGroup; - children = ( - DBC1338A051766837F4F534F /* IssuerAuthorizationExtensionHandler.swift */, - 1DAB657B3E3B0050DB3704AA /* Info.plist */, - 29F98636F7E05F181F8BEA43 /* PushProvisioningExtensionUI.entitlements */, - ); - name = PushProvisioningExtensionUI; - path = PushProvisioningExtensionUI; - sourceTree = ""; - }; - FBCFE0300596CE4AF772CF74 /* PushProvisioningExtension */ = { - isa = PBXGroup; - children = ( - A64C97DDF5A8819E8D49B613 /* IssuerExtensionHandler.swift */, - D4358D54E0FA0205DEF87539 /* Info.plist */, - C74EEB95F977642CDF8DEF29 /* PushProvisioningExtension.entitlements */, - ); - name = PushProvisioningExtension; - path = PushProvisioningExtension; - sourceTree = ""; - }; /* End PBXGroup section */ -/* Begin PBXContainerItemProxy section */ - 7A1C0DE7C11B0A4D2E5F3913 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 504EC2FC1FED79650016851F /* Project object */; - proxyType = 1; - remoteGlobalIDString = C6055E1181CCFF1463A4B1A9; - remoteInfo = PushProvisioningExtension; - }; - 7A1C0DE7C11B0A4D2E5F3914 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 504EC2FC1FED79650016851F /* Project object */; - proxyType = 1; - remoteGlobalIDString = 070043B7C4A54712203A6CFD; - remoteInfo = PushProvisioningExtensionUI; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXNativeTarget section */ - 070043B7C4A54712203A6CFD /* PushProvisioningExtensionUI */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6ACEED102A63081EBD1F9832 /* Build configuration list for PBXNativeTarget "PushProvisioningExtensionUI" */; - buildPhases = ( - D6D686A196E2A83343454AAA /* Sources */, - B35C2FC9E15503BC34FE55D5 /* Frameworks */, - 0EE4649BC94F38453693AF89 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = PushProvisioningExtensionUI; - productName = PushProvisioningExtensionUI; - productReference = 1F92515716B856A0CE9DF789 /* PushProvisioningExtensionUI.appex */; - productType = "com.apple.product-type.app-extension"; - }; 504EC3031FED79650016851F /* App */ = { isa = PBXNativeTarget; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; @@ -222,14 +92,10 @@ 504EC3001FED79650016851F /* Sources */, 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, - 7A1C0DE7C11B0A4D2E5F3907 /* Copy MeaWallet Config */, - 7A1C0DE7C11B0A4D2E5F3910 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( - 7A1C0DE7C11B0A4D2E5F3911 /* PBXTargetDependency */, - 7A1C0DE7C11B0A4D2E5F3912 /* PBXTargetDependency */, ); name = App; packageProductDependencies = ( @@ -240,23 +106,6 @@ productReference = 504EC3041FED79650016851F /* App.app */; productType = "com.apple.product-type.application"; }; - C6055E1181CCFF1463A4B1A9 /* PushProvisioningExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 90F0F421E339E9F41F22CFAF /* Build configuration list for PBXNativeTarget "PushProvisioningExtension" */; - buildPhases = ( - C220761294839A81DCF15C91 /* Sources */, - EE470AF53AAE4DD2C34C140B /* Frameworks */, - B025110FD2B134F7B38A23C0 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = PushProvisioningExtension; - productName = PushProvisioningExtension; - productReference = 41A22DB09E56D951D8F76C5C /* PushProvisioningExtension.appex */; - productType = "com.apple.product-type.app-extension"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -291,20 +140,11 @@ projectRoot = ""; targets = ( 504EC3031FED79650016851F /* App */, - C6055E1181CCFF1463A4B1A9 /* PushProvisioningExtension */, - 070043B7C4A54712203A6CFD /* PushProvisioningExtensionUI */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 0EE4649BC94F38453693AF89 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 504EC3021FED79650016851F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -318,45 +158,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B025110FD2B134F7B38A23C0 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 7A1C0DE7C11B0A4D2E5F3907 /* Copy MeaWallet Config */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Copy MeaWallet Config"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# mea_config is the MeaWallet MPP SDK environment config. It is gitignored\n# (CI writes it from a secret; local devs fetch it from 1Password), so it is\n# copied conditionally instead of living in the Resources phase — a missing\n# file must not fail builds that don't ship push provisioning.\nif [ -f \"${SRCROOT}/App/mea_config\" ]; then\n cp \"${SRCROOT}/App/mea_config\" \"${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/mea_config\"\n echo \"mea_config copied into app bundle\"\nelse\n echo \"mea_config not present — push provisioning disabled in this build\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 7A1C0DE7C11B0A4D2E5F3911 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C6055E1181CCFF1463A4B1A9 /* PushProvisioningExtension */; - targetProxy = 7A1C0DE7C11B0A4D2E5F3913 /* PBXContainerItemProxy */; - }; - 7A1C0DE7C11B0A4D2E5F3912 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 070043B7C4A54712203A6CFD /* PushProvisioningExtensionUI */; - targetProxy = 7A1C0DE7C11B0A4D2E5F3914 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXSourcesBuildPhase section */ 504EC3001FED79650016851F /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -365,25 +168,6 @@ 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 7A1C0DE7C11B0A4D2E5F3901 /* AppViewController.swift in Sources */, 7A1C0DE7C11B0A4D2E5F3903 /* ClipboardDetectPlugin.swift in Sources */, - 7A1C0DE7C11B0A4D2E5F3905 /* PushProvisioningPlugin.swift in Sources */, - 280163DBDEA95EE6C90C5450 /* WalletExtensionCardStore.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C220761294839A81DCF15C91 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - BF9C96F67827E0FE790DF482 /* IssuerExtensionHandler.swift in Sources */, - 7A668B3F8C26297AC5BEC8AE /* WalletExtensionCardStore.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D6D686A196E2A83343454AAA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - FE137B7240B844B8CABFD47C /* IssuerAuthorizationExtensionHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -409,24 +193,6 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 44109A9A35C1732B284E5D39 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - CODE_SIGN_ENTITLEMENTS = PushProvisioningExtensionUI/PushProvisioningExtensionUI.entitlements; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = PushProvisioningExtensionUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0.53; - PRODUCT_BUNDLE_IDENTIFIER = me.peanut.wallet.PushProvisioningExtensionUI; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; 504EC3141FED79650016851F /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; @@ -541,9 +307,9 @@ baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 1; CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -564,11 +330,12 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = App/AppRelease.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; + CODE_SIGN_IDENTITY = "Apple Distribution"; DEVELOPMENT_TEAM = PW388G893L; + PROVISIONING_PROFILE_SPECIFIER = "Peanut Wallet App Store"; + CURRENT_PROJECT_VERSION = 1; CODE_SIGN_ENTITLEMENTS = App/AppRelease.entitlements; + INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -578,77 +345,12 @@ MARKETING_VERSION = 1.0.53; PRODUCT_BUNDLE_IDENTIFIER = me.peanut.wallet; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = "Peanut Wallet App Store"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; - 6A9099E8585767FFD6C742CF /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - CODE_SIGN_ENTITLEMENTS = PushProvisioningExtensionUI/PushProvisioningExtensionUI.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PW388G893L; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = PushProvisioningExtensionUI/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0.53; - PRODUCT_BUNDLE_IDENTIFIER = me.peanut.wallet.PushProvisioningExtensionUI; - PROVISIONING_PROFILE_SPECIFIER = "Peanut Wallet Provisioning Extension UI App Store"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 84DB4EDBA73BB3F765C74540 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - CODE_SIGN_ENTITLEMENTS = PushProvisioningExtension/PushProvisioningExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Distribution"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PW388G893L; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = PushProvisioningExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0.53; - PRODUCT_BUNDLE_IDENTIFIER = me.peanut.wallet.PushProvisioningExtension; - PROVISIONING_PROFILE_SPECIFIER = "Peanut Wallet Provisioning Extension App Store"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - E62BF018FC33CA4AD488EED2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - CODE_SIGN_ENTITLEMENTS = PushProvisioningExtension/PushProvisioningExtension.entitlements; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = PushProvisioningExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0.53; - PRODUCT_BUNDLE_IDENTIFIER = me.peanut.wallet.PushProvisioningExtension; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -670,24 +372,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 6ACEED102A63081EBD1F9832 /* Build configuration list for PBXNativeTarget "PushProvisioningExtensionUI" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6A9099E8585767FFD6C742CF /* Release */, - 44109A9A35C1732B284E5D39 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 90F0F421E339E9F41F22CFAF /* Build configuration list for PBXNativeTarget "PushProvisioningExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 84DB4EDBA73BB3F765C74540 /* Release */, - E62BF018FC33CA4AD488EED2 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements index c2b76f320e..fd2bcc8741 100644 --- a/ios/App/App/App.entitlements +++ b/ios/App/App/App.entitlements @@ -12,7 +12,6 @@ com.apple.security.application-groups group.me.peanut.wallet.onesignal - group.me.peanut.wallet diff --git a/ios/App/App/AppRelease.entitlements b/ios/App/App/AppRelease.entitlements index 258c54f16a..c6ec3ac040 100644 --- a/ios/App/App/AppRelease.entitlements +++ b/ios/App/App/AppRelease.entitlements @@ -12,7 +12,6 @@ com.apple.security.application-groups group.me.peanut.wallet.onesignal - group.me.peanut.wallet diff --git a/scripts/__tests__/android-version-code.test.js b/scripts/__tests__/android-version-code.test.js new file mode 100644 index 0000000000..1668646435 --- /dev/null +++ b/scripts/__tests__/android-version-code.test.js @@ -0,0 +1,80 @@ +const { spawnSync } = require('child_process') +const path = require('path') + +const SCRIPT_PATH = path.join(__dirname, '..', 'android-version-code.mjs') + +// The script is a CI entrypoint: its contract is stdout + exit code, so run it +// the way android-release.yml does instead of reaching into its internals. +function run(...args) { + return spawnSync(process.execPath, [SCRIPT_PATH, ...args], { encoding: 'utf-8' }) +} + +function codeAt(iso) { + const result = run('--now', iso) + expect(result.status).toBe(0) + return Number(result.stdout.trim()) +} + +describe('android-version-code', () => { + it('clears every legacy code Play has already seen', () => { + // Small console codes, git-commit-count builds to ~8600, and the + // run_number era up to the live 10048. + expect(codeAt('2026-09-04T12:00:00Z')).toBeGreaterThan(10048) + }) + + it('stays under Play maximum', () => { + expect(codeAt('2090-01-01T00:00:00Z')).toBeLessThanOrEqual(2100000000) + }) + + it('increases with upload time, whatever order the runs were dispatched in', () => { + // The exact sequence that wedged the previous scheme: dispatch 1 + // uploads, dispatch 2 uploads later, then "Re-run failed jobs" is + // pressed on dispatch 1. GitHub preserves run_number for that re-run + // and only bumps run_attempt, so a run-numbered code went BACKWARDS and + // Play refused the recovery upload. Wall-clock cannot: the re-run + // uploads last, so it is highest. + const dispatchOne = codeAt('2026-09-04T10:00:00Z') + const dispatchTwo = codeAt('2026-09-04T11:00:00Z') + const rerunOfDispatchOne = codeAt('2026-09-04T12:00:00Z') + + expect(dispatchTwo).toBeGreaterThan(dispatchOne) + expect(rerunOfDispatchOne).toBeGreaterThan(dispatchTwo) + }) + + it('separates two uploads a second apart', () => { + // A real build is 10+ minutes and the release workflow holds a mutex, + // so this is headroom rather than a scenario — but per-second means no + // pair of uploads can ever collide. + expect(codeAt('2026-09-04T12:00:01Z')).toBe(codeAt('2026-09-04T12:00:00Z') + 1) + }) + + it('does not depend on the version, which is what could not stay ordered', () => { + // Deliberate: a .-derived code cannot be made monotonic + // across an out-of-order re-run. versionName carries the human version; + // versionCode is an opaque ordering key. + expect(run('--now', '2026-09-04T12:00:00Z').stdout.trim()).toBe( + run('--now', '2026-09-04T12:00:00Z').stdout.trim() + ) + }) + + it('refuses a clock before the epoch rather than emitting a rejected code', () => { + const result = run('--now', '2025-01-01T00:00:00Z') + + expect(result.status).toBe(1) + expect(result.stderr).toContain('legacy floor') + }) + + it('refuses an unparseable date instead of defaulting to now', () => { + const result = run('--now', 'not-a-date') + + expect(result.status).toBe(1) + expect(result.stderr).toContain('is not a date') + }) + + it('rejects --now with no value rather than silently using the current time', () => { + const result = run('--now') + + expect(result.status).toBe(1) + expect(result.stderr).toContain('needs a date') + }) +}) diff --git a/scripts/android-version-code.mjs b/scripts/android-version-code.mjs new file mode 100644 index 0000000000..55cfe14670 --- /dev/null +++ b/scripts/android-version-code.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +// The Play versionCode for a release upload: seconds since 2026-01-01 UTC. +// +// Play's only real requirement is that the code strictly increases, and the +// hard part is that it must increase **in upload order**, which is not the same +// as run order. Two earlier attempts got this wrong: +// +// $((10000 + GITHUB_RUN_NUMBER)) — inside a `workflow_call` run_number is the +// CALLER's counter, so the first release-native.yml run numbered itself 1 +// and shipped 10001 against a live 10048. Play refused it. +// +// *10000000 + *10000 + run_number*10 + run_attempt +// — monotonic within one run lineage, but GitHub PRESERVES run_number when +// you re-run an older workflow and only bumps run_attempt. Dispatch 1 +// uploads …010, dispatch 2 uploads …020, then "Re-run failed jobs" on +// dispatch 1 computes …011 — below what Play already has, so the recovery +// upload is refused and the retry wedge stays exactly where it was. +// +// Wall-clock is the only value that increases in upload order no matter how the +// runs are ordered, re-run, or interleaved. It is monotonic by construction +// rather than by an argument about GitHub's numbering, which is the property +// that kept being wrong. +// +// The cost, stated plainly: the code no longer encodes the version. That +// structure is what cannot survive an out-of-order re-run, so it had to go — +// versionName is what humans read in Play, and versionCode is an opaque +// ordering key, which is exactly what Google documents it as. +// +// Usage: +// node scripts/android-version-code.mjs +// node scripts/android-version-code.mjs --now 2026-09-04T12:00:00Z # tests + +// Recent enough that the count clears every legacy code on Play (small console +// codes, git-commit-count builds to ~8600, and the run_number era to 10048), +// and far enough from Play's 2100000000 ceiling for ~66 years. +const EPOCH_SECONDS = Date.UTC(2026, 0, 1) / 1000 + +// The highest code Play has already seen from the retired schemes. A computed +// code at or below it would be refused, and failing here beats a 30-minute +// build that dies at the upload step. +const LEGACY_FLOOR = 10048 + +const PLAY_MAX = 2100000000 + +export function versionCodeFor(now = new Date()) { + const seconds = Math.floor(now.getTime() / 1000) - EPOCH_SECONDS + + if (!Number.isFinite(seconds)) throw new Error(`could not read a time from "${now}"`) + if (seconds <= LEGACY_FLOOR) { + throw new Error( + `versionCode ${seconds} does not clear the legacy floor ${LEGACY_FLOOR} — is the runner clock before ${new Date(EPOCH_SECONDS * 1000).toISOString()}?` + ) + } + if (seconds > PLAY_MAX) throw new Error(`versionCode ${seconds} exceeds Play's maximum ${PLAY_MAX}`) + + return seconds +} + +function flag(argv, name) { + const index = argv.indexOf(name) + return index === -1 ? undefined : argv[index + 1] +} + +function main(argv) { + const now = flag(argv, '--now') + if (argv.includes('--now') && !now) throw new Error('--now needs a date') + const at = now === undefined ? new Date() : new Date(/^\d+$/.test(now) ? Number(now) * 1000 : now) + if (Number.isNaN(at.getTime())) throw new Error(`"${now}" is not a date`) + return String(versionCodeFor(at)) +} + +if (process.argv[1] && process.argv[1].endsWith('android-version-code.mjs')) { + try { + process.stdout.write(`${main(process.argv.slice(2))}\n`) + } catch (err) { + console.error(`✗ android-version-code: ${err.message}`) + process.exit(1) + } +}