Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9c6d439
Merge origin/main into feat/app-localization
innolope-dev Jul 17, 2026
7bb149a
fix(android): break the post-logout stuck-splash reload loop
innolope-dev Jul 17, 2026
58be2e4
fix(setup): fill the bottom inset white on Android too
innolope-dev Jul 17, 2026
f52be9a
fix(mascot): serve the GIF fallbacks on Android native
innolope-dev Jul 17, 2026
fd5dd5b
perf(modal): promote the dialog panel to a compositor layer
innolope-dev Jul 17, 2026
14b2460
fix(login): don't paint /home before the passkey ceremony finishes
innolope-dev Jul 17, 2026
094accd
fix(setup): auto-continue completed sessions past the sign-in interst…
innolope-dev Jul 17, 2026
1e09667
feat(auth): revoke the session server-side on logout
innolope-dev Jul 17, 2026
8a7d040
fix(native): ship production APNs entitlements and require FCM config
innolope-dev Jul 17, 2026
969dbeb
fix(notifications): surface OneSignal login failures and add debug lo…
innolope-dev Jul 17, 2026
1f75fbe
merge: origin/main into feat/mobile-release
innolope-dev Jul 17, 2026
8b27d28
merge: fix/android-release-bugs into feat/mobile-release
innolope-dev Jul 17, 2026
1cad015
merge: fix/native-push-notifications into feat/mobile-release
innolope-dev Jul 17, 2026
c6d3291
fix(android): make google-services.json optional again
innolope-dev Jul 17, 2026
2ecf061
fix(android): remove google-services.json from the build entirely
innolope-dev Jul 17, 2026
b9c64ee
fix(native-build): recognize P0_TRANSFORMS routes in the anti-rot guard
innolope-dev Jul 17, 2026
b1efa5c
fix(ci): stop setting changesNotSentForReview on the Play upload
innolope-dev Jul 17, 2026
f0fd4fc
fix(ios-release): fail fast when cap sync drops the SumSub Cordova pl…
innolope-dev Jul 17, 2026
d0ea330
fix(ios-release): derive MARKETING_VERSION from the pushed tag
innolope-dev Jul 17, 2026
bf1883f
chore(ci): point capgo iOS OTA trigger at renamed mobile-release branch
innolope-dev Jul 17, 2026
9480460
test(kyc): wrap SumsubKycWrapper tests in NextIntlClientProvider
innolope-dev Jul 17, 2026
18886e2
fix(types): type-only ReactNode import in SumsubKycWrapper test
innolope-dev Jul 17, 2026
48b1c38
fix(native): widen the R5F fetch diagnostics and evict stale WebView SWs
innolope-dev Jul 18, 2026
c490bc0
chore(mobile-release): bump version to 1.0.32, default capgo dispatch…
innolope-dev Jul 20, 2026
302edd8
fix(native): fall back to CapacitorHttp transport when WebView fetch …
innolope-dev Jul 21, 2026
3897e63
fix(ui): hide all banners in demo mode
innolope-dev Jul 21, 2026
71c285d
chore(mobile-release): bump version to 1.0.33 for the OTA bundle
innolope-dev Jul 21, 2026
00a3c11
chore(mobile-release): bump to 1.0.34 — 1.0.33 already claimed in Capgo
innolope-dev Jul 21, 2026
28a327d
chore(mobile-release): 1.0.35 — native rebuild carrying the transport…
innolope-dev Jul 21, 2026
b3c680c
merge: main into mobile-release (pre-build sync)
innolope-dev Jul 22, 2026
2e241a3
merge: localization + eslint cleanup (#2447) into mobile-release
innolope-dev Jul 22, 2026
e53aedd
fix(balance): keep last-known spendable total instead of rendering $0
innolope-dev Jul 22, 2026
8ac72ae
fix(balance): consume balanceUnavailable, stop polling /rain/cards fo…
innolope-dev Jul 22, 2026
1d9cf30
fix(balance): share the rain-known predicate, stop rendering unknown …
innolope-dev Jul 22, 2026
4e179b4
test(balance): drop import/first disable — rule not registered on thi…
innolope-dev Jul 22, 2026
91e6c73
chore(mobile-release): 1.0.36 — main sync + localization + $0-balance…
innolope-dev Jul 23, 2026
ff49260
feat(security): move the native session token into biometric-guarded …
innolope-dev Jul 23, 2026
822dbb2
fix(build): keep React out of the app-lock registry module graph
innolope-dev Jul 23, 2026
a974e9f
fix(lint): type window.Capacitor.isPluginAvailable instead of casting…
innolope-dev Jul 23, 2026
dab8c99
fix(app-lock): friendlier lock-screen copy in all locales
innolope-dev Jul 23, 2026
f38dd83
feat(app-lock): gate app-open lock behind OPEN_GATED (default off)
innolope-dev Jul 24, 2026
3837f2a
test(app-lock): run gate tests with OPEN_GATED on; cover default pass…
innolope-dev Jul 24, 2026
27bf61e
test(app-lock): default OPEN_GATED off in tests, opt in per lock case
innolope-dev Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
23 changes: 7 additions & 16 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ on:
description: 'versionName override (optional; defaults to package.json)'
required: false
type: string
pushDebug:
description: 'Enable OneSignal verbose logging in this build'
required: false
default: false
type: boolean

permissions:
contents: read
Expand Down Expand Up @@ -104,19 +109,8 @@ jobs:
NEXT_PUBLIC_SAFARI_WEB_ID=${{ vars.NEXT_PUBLIC_SAFARI_WEB_ID }}
NEXT_PUBLIC_ONESIGNAL_WEBHOOK=${{ vars.NEXT_PUBLIC_ONESIGNAL_WEBHOOK }}
EOF

- name: Decode google-services.json
# FCM credentials for native push. When the secret is unset the file
# is skipped and build.gradle simply omits the google-services plugin
# (push disabled, build still succeeds).
env:
ANDROID_GOOGLE_SERVICES_JSON: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON }}
run: |
if [ -n "$ANDROID_GOOGLE_SERVICES_JSON" ]; then
echo "$ANDROID_GOOGLE_SERVICES_JSON" | base64 -d > android/app/google-services.json
echo "✅ google-services.json written"
else
echo "⚠️ ANDROID_GOOGLE_SERVICES_JSON unset — native push disabled for this build"
if [ "${{ github.event.inputs.pushDebug }}" = "true" ]; then
echo "NEXT_PUBLIC_ONESIGNAL_DEBUG=true" >> .env.production.local
fi

- name: Build signed AAB
Expand Down Expand Up @@ -144,9 +138,6 @@ jobs:
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
tracks: ${{ github.event.inputs.track || 'internal' }}
status: completed
# First releases have no reviewed base, so Play can't auto-submit
# for review; commit the edit and review from the Console instead.
changesNotSentForReview: true
# Staged production rollout: set status: inProgress + userFraction: 0.1,
# then promote in Play Console once crash/error rates look clean.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/capgo-deploy-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: Deploy OTA Update — iOS only (Capgo)

on:
push:
branches: [feat/mobile-release]
branches: [mobile-release]
workflow_dispatch:
inputs:
channel:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/capgo-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
channel:
description: 'Capgo channel to deploy to'
required: true
default: 'staging'
default: 'production'
type: choice
options:
- development
Expand Down
52 changes: 48 additions & 4 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ on:
description: 'versionName override (optional; defaults to project MARKETING_VERSION)'
required: false
type: string
pushDebug:
description: 'Enable OneSignal verbose logging in this build'
required: false
default: false
type: boolean

permissions:
contents: read
Expand Down Expand Up @@ -60,6 +65,20 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Verify SumSub Cordova plugin materialized
# `cap sync ios` silently drops a Cordova plugin (exit 0, no folder)
# when node_modules isn't fully materialized, and the archive then
# fails a minute later with a cryptic SwiftPM error. Catch the real
# cause — a partial pnpm install — here, where a re-run recovers.
run: |
P=node_modules/@sumsub/cordova-idensic-mobile-sdk-plugin
if [ ! -f "$P/package.json" ] || [ ! -f "$P/plugin.xml" ]; then
echo "::error::$P is not fully installed (package.json/plugin.xml missing). This is the intermittent iOS-release flake — cap sync would drop the plugin and the archive would fail cryptically. Re-run the job (pnpm install did not materialize the plugin)."
ls -la "$P" 2>&1 || true
exit 1
fi
echo "SumSub Cordova plugin materialized."

- name: Production web env
run: |
# NEXT_PUBLIC_* values are baked into the static export at build time.
Expand All @@ -83,6 +102,9 @@ jobs:
NEXT_PUBLIC_SAFARI_WEB_ID=${{ vars.NEXT_PUBLIC_SAFARI_WEB_ID }}
NEXT_PUBLIC_ONESIGNAL_WEBHOOK=${{ vars.NEXT_PUBLIC_ONESIGNAL_WEBHOOK }}
EOF
if [ "${{ github.event.inputs.pushDebug }}" = "true" ]; then
echo "NEXT_PUBLIC_ONESIGNAL_DEBUG=true" >> .env.production.local
fi

- name: Build web + sync iOS
run: |
Expand Down Expand Up @@ -110,6 +132,13 @@ jobs:
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.
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

- name: Archive & export IPA
env:
Expand All @@ -118,11 +147,15 @@ jobs:
# Monotonic, always-increments-even-on-rerun. TestFlight requires each
# upload's build number to exceed the last.
IOS_BUILD_NUMBER: ${{ github.run_number }}
IOS_VERSION_NAME: ${{ github.event.inputs.versionName || '' }}
IOS_VERSION_INPUT: ${{ github.event.inputs.versionName || '' }}
IOS_TAG_NAME: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
# Only override MARKETING_VERSION when an explicit versionName is supplied;
# otherwise keep the project's MARKETING_VERSION. CURRENT_PROJECT_VERSION is
# always the CI run number. Info.plist reads both via $(...) build settings.
# MARKETING_VERSION comes from the explicit versionName input, else from
# the pushed tag (v1.0.31 -> 1.0.31); a bare tag push must not silently
# ship the project default 1.0. CURRENT_PROJECT_VERSION is always the CI
# run number. Info.plist reads both via $(...) build settings.
IOS_VERSION_NAME="$IOS_VERSION_INPUT"
if [ -z "$IOS_VERSION_NAME" ] && [ -n "$IOS_TAG_NAME" ]; then IOS_VERSION_NAME="${IOS_TAG_NAME#v}"; fi
MARKETING_ARG=()
if [ -n "$IOS_VERSION_NAME" ]; then MARKETING_ARG=("MARKETING_VERSION=$IOS_VERSION_NAME"); fi

Expand Down Expand Up @@ -171,6 +204,17 @@ jobs:
-exportPath build/ios \
-exportOptionsPlist /tmp/ExportOptions.plist

- name: Verify push entitlements in exported IPA
run: |
unzip -q build/ios/App.ipa -d /tmp/ipa-check
APS=$(codesign -d --entitlements :- /tmp/ipa-check/Payload/App.app 2>/dev/null \
| plutil -extract aps-environment raw -o - - || true)
echo "aps-environment=$APS"
if [ "$APS" != "production" ]; then
echo "::error::exported ipa aps-environment is '${APS:-missing}', expected production — push would be dead in this build"
exit 1
fi

- name: Upload to TestFlight
uses: apple-actions/upload-testflight-build@1ad58030672057aa084b4e96beb6f7a8c627f9e6 # v5
with:
Expand Down
12 changes: 4 additions & 8 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,7 @@ dependencies {

apply from: 'capacitor.build.gradle'

try {
def servicesJSON = file('google-services.json')
if (servicesJSON.text) {
apply plugin: 'com.google.gms.google-services'
}
} catch(Exception e) {
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}
// No google-services plugin: OneSignal registers with FCM on its own —
// PushRegistratorFCM builds FirebaseOptions in code from the sender ID served
// by the OneSignal dashboard, and firebase-messaging arrives transitively via
// com.onesignal:notifications. See docs/NATIVE-RELEASE.md §Android push.
4 changes: 4 additions & 0 deletions android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-camera')
implementation project(':capacitor-clipboard')
implementation project(':capacitor-device')
implementation project(':capacitor-haptics')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-preferences')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
implementation project(':capgo-capacitor-crisp')
implementation project(':capgo-capacitor-native-biometric')
implementation project(':capgo-capacitor-passkey')
implementation project(':capgo-capacitor-updater')
implementation project(':onesignal-capacitor-plugin')
Expand Down
33 changes: 29 additions & 4 deletions android/app/src/main/java/me/peanut/wallet/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Bridge;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MainActivity extends BridgeActivity {
Expand Down Expand Up @@ -55,7 +57,7 @@ private WebResourceResponse findPageHtml(WebView view, String path) {
// 1. try exact path
try {
String cleanPath = path.endsWith("/") ? path : path + "/";
InputStream is = view.getContext().getAssets().open("public" + cleanPath + "index.html");
InputStream is = openAppContent(view, cleanPath);
return new WebResourceResponse("text/html", "UTF-8", is);
} catch (Exception ignored) {}

Expand All @@ -69,7 +71,7 @@ private WebResourceResponse findPageHtml(WebView view, String path) {
String tryPath = String.join("/", segments);
if (!tryPath.endsWith("/")) tryPath += "/";
try {
InputStream is = view.getContext().getAssets().open("public" + tryPath + "index.html");
InputStream is = openAppContent(view, tryPath);
return new WebResourceResponse("text/html", "UTF-8", is);
} catch (Exception ignored) {
segments[i] = original;
Expand All @@ -85,19 +87,42 @@ private WebResourceResponse findPageHtml(WebView view, String path) {
parentPath = parentPath.substring(0, parentPath.lastIndexOf("/"));
if (parentPath.isEmpty()) break;
try {
InputStream is = view.getContext().getAssets().open("public" + parentPath + "/index.html");
InputStream is = openAppContent(view, parentPath + "/");
return new WebResourceResponse("text/html", "UTF-8", is);
} catch (Exception ignored) {}
}

// 4. root fallback
try {
InputStream is = view.getContext().getAssets().open("public/index.html");
InputStream is = openAppContent(view, "/");
return new WebResourceResponse("text/html", "UTF-8", is);
} catch (Exception ignored) {}

return null;
}

/**
* Opens the index.html for a directory-style path ("/setup/"),
* honoring an active OTA bundle. When CapacitorUpdater has
* pointed the server base path at an on-disk bundle, HTML must
* come from that bundle — the APK's assets are a stale export
* whose chunk references no longer exist, and serving them
* bricks navigation (stuck splash loop after logout). Only when
* no bundle is active (base path isn't a directory) do we read
* the bundled assets.
*/
private InputStream openAppContent(WebView view, String cleanPath) throws Exception {
String rel = (cleanPath.startsWith("/") ? cleanPath.substring(1) : cleanPath) + "index.html";
Bridge activeBridge = getBridge();
String basePath = activeBridge != null ? activeBridge.getServerBasePath() : null;
if (basePath != null && !basePath.isEmpty()) {
File base = new File(basePath);
if (base.isDirectory()) {
return new FileInputStream(new File(base, rel));
}
}
return view.getContext().getAssets().open("public/" + rel);
}
});
}
}
Expand Down
1 change: 0 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath 'com.google.gms:google-services:4.4.4'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand Down
12 changes: 12 additions & 0 deletions android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,24 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
include ':capacitor-browser'
project(':capacitor-browser').projectDir = new File('../node_modules/.pnpm/@capacitor+browser@8.0.3_@capacitor+core@8.2.0/node_modules/@capacitor/browser/android')

include ':capacitor-camera'
project(':capacitor-camera').projectDir = new File('../node_modules/.pnpm/@capacitor+camera@8.2.0_@capacitor+core@8.2.0/node_modules/@capacitor/camera/android')

include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@8.0.1_@capacitor+core@8.2.0/node_modules/@capacitor/clipboard/android')

include ':capacitor-device'
project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@8.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/device/android')

include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@8.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/haptics/android')

include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../node_modules/.pnpm/@capacitor+keyboard@8.0.3_@capacitor+core@8.2.0/node_modules/@capacitor/keyboard/android')

include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@8.0.1_@capacitor+core@8.2.0/node_modules/@capacitor/preferences/android')

include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.2.0/node_modules/@capacitor/splash-screen/android')

Expand All @@ -26,6 +35,9 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@c
include ':capgo-capacitor-crisp'
project(':capgo-capacitor-crisp').projectDir = new File('../node_modules/.pnpm/@capgo+capacitor-crisp@8.0.27_@capacitor+core@8.2.0/node_modules/@capgo/capacitor-crisp/android')

include ':capgo-capacitor-native-biometric'
project(':capgo-capacitor-native-biometric').projectDir = new File('../node_modules/.pnpm/@capgo+capacitor-native-biometric@8.6.0_@capacitor+core@8.2.0/node_modules/@capgo/capacitor-native-biometric/android')

include ':capgo-capacitor-passkey'
project(':capgo-capacitor-passkey').projectDir = new File('../node_modules/@capgo/capacitor-passkey/android')

Expand Down
7 changes: 6 additions & 1 deletion capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ const config: CapacitorConfig = {
publicKey:
'-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAr0HzEca/1vuvWcJ8/xYB6tx0j4uJMzw/kT34GnjyMlRmLLUIO9sj\nroXaUGaNoqlOCx73b7Qgp10TLPOAVxmoHV9ZJ4BS9cMCl5mvzB4qIdl6FZLcl3g5\nk5Nkj4w22nskqbBqL7eqMXpk4DD9oWRclnaZC/lCpok1n2AWy4EMZrshemBQ6iXr\ncppo+WByPbqmh/GbHvJyRvkx4Rgt2LSBJBI3laP3eEDkujCq1ZH9qgcIE4MXO5xq\n7c6LsLjN5wkQiNPSPI81zAbqBThhqodKzwav0FwIE1pyiJeGk1nV5Ji5kUgpFNwY\nY78iDVq4OP2jPfWO4jXnnJtnGN7aeKDMEQIDAQAB\n-----END RSA PUBLIC KEY-----\n',
},
// CapacitorHttp OFF: its Android GET proxy (_capacitor_http_interceptor_)
// stalls under load, timing out every in-flight request (PEANUT-UI-R44).
// Requests go direct from the webview — CORS allows https://localhost and
// capacitor://, and auth rides the Authorization header from native
// Preferences (src/utils/auth-token.ts) instead of the plugin's cookie jar.
CapacitorHttp: {
enabled: true,
enabled: false,
},
CapacitorPasskey: {
// shim patches navigator.credentials.create/get so browser WebAuthn code works natively.
Expand Down
Loading
Loading