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)
+ }
+}