Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis pull request introduces comprehensive Android support via Capacitor, adds a native build orchestration script, integrates WebAuthn functionality via capacitor-webauthn, and creates a "Native PoC" page demonstrating end-to-end passkey workflows with ZeroDev account abstraction. It also bypasses passkey support detection checks across the codebase. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Important Merge conflicts detected (Beta)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Home/HomePerkClaimSection.tsx (1)
52-66:⚠️ Potential issue | 🟡 MinorResolve the Prettier failure before merge readiness.
CI reports a formatting warning; please run the formatter on this file/change so the pipeline is clean.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Home/HomePerkClaimSection.tsx` around lines 52 - 66, The file fails Prettier formatting; run your project formatter (e.g., prettier --write) on HomePerkClaimSection.tsx or reformat the useWebSocket call and its callback so it matches project style (ensure consistent indentation, spacing and trailing commas) around useWebSocket, useCallback, queryClient.invalidateQueries, setArrivalSource and setArrivalPhase; then re-run CI to confirm the Prettier warning is resolved.
🧹 Nitpick comments (6)
android/app/src/main/res/layout/activity_main.xml (1)
1-12: Layout may be unused by Capacitor's BridgeActivity.Capacitor's
BridgeActivitymanages its own WebView programmatically and does not use this layout file. This is harmless boilerplate from Android Studio scaffolding, but can be removed to avoid confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/main/res/layout/activity_main.xml` around lines 1 - 12, The activity_main.xml layout includes a WebView that is likely unused because Capacitor's BridgeActivity manages its WebView programmatically; remove or archive this boilerplate layout to avoid confusion—specifically delete or move android/app/src/main/res/layout/activity_main.xml (the <WebView> entry and surrounding CoordinatorLayout) or add a comment indicating it is unused, and ensure no references to activity_main.xml remain in AndroidManifest or MainActivity so BridgeActivity continues to supply the WebView.public/.well-known/assetlinks.json (1)
1-10: Debug signing fingerprint - must be replaced for production.The
sha256_cert_fingerprintsvalue appears to be a development/debug signing key fingerprint. For production releases, this must be updated to match the release keystore fingerprint, otherwise passkey association will fail on production builds.Consider adding a comment or documentation noting this requirement, or automate fingerprint injection during build pipelines.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@public/.well-known/assetlinks.json` around lines 1 - 10, The assetlinks.json currently contains a debug signing fingerprint in the sha256_cert_fingerprints array which must be replaced for production; update the value in public/.well-known/assetlinks.json so the "sha256_cert_fingerprints" entry under the "target" for package_name "com.peanut.app" uses the release keystore SHA-256 fingerprint (or modify your build to inject the correct fingerprint), and optionally add a brief comment or README note near assetlinks.json indicating fingerprints must be replaced/automatically injected for production releases.src/app/page.tsx (1)
197-204: Temporary debug link on production landing page.The comment correctly marks this as temporary, but since it's on the main landing page, consider gating it behind an environment check to prevent accidental production deployment:
🛡️ Proposed environment-gated rendering
- {/* TEMPORARY: Native PoC Link - Remove after testing */} - <Link - href="/native-poc" - className="fixed top-4 right-4 z-50 rounded-lg border-2 border-dashed border-purple-400 bg-purple-100 px-4 py-2 text-sm font-bold text-purple-700 shadow-lg" - > - 🧪 Native PoC - </Link> + {/* TEMPORARY: Native PoC Link - Remove after testing */} + {process.env.NODE_ENV === 'development' && ( + <Link + href="/native-poc" + className="fixed top-4 right-4 z-50 rounded-lg border-2 border-dashed border-purple-400 bg-purple-100 px-4 py-2 text-sm font-bold text-purple-700 shadow-lg" + > + 🧪 Native PoC + </Link> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/page.tsx` around lines 197 - 204, The temporary Native PoC Link JSX in page.tsx is rendered unconditionally on the landing page; wrap the <Link ...> block in an environment check so it only renders in non-production or when an explicit opt-in flag is set (e.g., process.env.NODE_ENV !== 'production' or process.env.NEXT_PUBLIC_ENABLE_NATIVE_POC === 'true'). Update the conditional where the Link component appears to gate rendering, and ensure you use a NEXT_PUBLIC_ prefixed env var if the check runs client-side so the variable is available in the browser; default to not rendering when the env var is missing.android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java (1)
1-17: Drop or rename the template test scaffold.This is still the stock Capacitor sample (
com.getcapacitor.myapp+2 + 2 == 4), so it doesn't protect the new Android integration and will just add noise once real tests land.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java` around lines 1 - 17, The current unit test is the default Capacitor template (package com.getcapacitor.myapp, class ExampleUnitTest, method addition_isCorrect) and should be removed or replaced; either delete this test file entirely or rename ExampleUnitTest and replace addition_isCorrect with meaningful Android integration/unit tests relevant to our app (or add a TODO and a real test scaffolding) so the placeholder 2+2==4 test does not clutter CI or mask regressions.scripts/native-build.js (1)
194-224: Trap interruptions before mutating the worktree in place.If this process is interrupted after
disableItems()orwrapClientComponents(), the repo is left with renamed*.disabled/_ClientPage.tsxfiles. Consider trappingSIGINT/SIGTERMor doing the rewrite in a temp copy if this script is going to be reused beyond the demo branch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/native-build.js` around lines 194 - 224, The script mutates the worktree in-place via disableItems() and wrapClientComponents() without handling interruptions; trap SIGINT and SIGTERM in main so that on signal you call restoreModifiedFiles() and restoreItems() (and set process.exitCode) before exiting, or alternatively perform the file rewrites in a temporary copy and run the build there and always call restoreModifiedFiles()/restoreItems() in a finally/cleanup handler; update main to register signal handlers that reference disableItems/wrapClientComponents/restoreModifiedFiles/restoreItems (and remove handlers after completion) to ensure the repo is restored on interrupt.src/app/(mobile-ui)/native-poc/page.tsx (1)
719-722: Preserve multiline debug output.Most
testResultvalues contain\n, but this container collapses whitespace, so the logs render as one long line.whitespace-pre-wrapwould make the PoC output readable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(mobile-ui)/native-poc/page.tsx around lines 719 - 722, The debug output container for testResult collapses newlines; update the JSX element that renders testResult (the div that reads {testResult && (...)} and uses classes including rounded p-2 text-sm break-all) to preserve multiline output by adding the whitespace-pre-wrap utility (e.g., include "whitespace-pre-wrap" in the className list alongside or instead of break-all) so \n in testResult renders as line breaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`:
- Line 24: The test ExampleInstrumentedTest contains a stale package assertion
using appContext.getPackageName(); update the expected literal from
"com.getcapacitor.app" to the actual app id "com.peanut.app" so the assertion in
the test method (in ExampleInstrumentedTest) reflects the new package name used
by the app.
In `@android/app/src/main/AndroidManifest.xml`:
- Line 5: Change the Android manifest's backup setting for the passkey-enabled
WebView wrapper by setting the android:allowBackup attribute to false (replace
android:allowBackup="true" with android:allowBackup="false") in the
AndroidManifest so app data is not included in device/cloud backups;
alternatively, add explicit fullBackupContent or dataExtractionRules that
exclude authentication state if you prefer to keep allowBackup true.
In `@android/app/src/main/res/xml/file_paths.xml`:
- Around line 2-5: The FileProvider paths in file_paths.xml are too permissive
(external-path name="my_images" path="." and cache-path name="my_cache_images"
path=".")—restrict them to app-specific subdirectories instead: create and use a
dedicated external media folder (e.g., Pictures/YourApp or
Android/data/<package>/files/Pictures) for my_images and a dedicated cache
subfolder for my_cache_images, then update the path attributes to those specific
relative paths and ensure your app creates/uses those directories when sharing
files; this limits FileProvider URIs to only the files your app actually needs
to expose.
In `@capacitor.config.ts`:
- Around line 14-17: The Android webview debug and mixed-content settings are
enabled (android.allowMixedContent and android.webContentsDebuggingEnabled)
which must be disabled for production; update the configuration to set these
flags false by default and make them conditional on a development flag (e.g.,
process.env.NODE_ENV === "development" or an env var like DEBUG_WEBVIEW) so they
remain true only in dev builds, or ensure release builds explicitly set
android.allowMixedContent = false and android.webContentsDebuggingEnabled =
false (adjust the exported capacitor config or initialization logic that builds
the android config object to apply this conditional).
- Around line 9-12: The hardcoded ngrok URL in capacitor.config.ts (server.url)
will break for other developers; change server.url to read from an environment
variable (e.g., process.env.CAPACITOR_SERVER_URL) with a sensible fallback (or
omit the url entirely for local dev), and update the project README to document
how to set CAPACITOR_SERVER_URL for development. Ensure the code that reads
server.url handles missing/empty env values gracefully so other devs aren’t
forced to use the original ngrok address.
In `@next.config.js`:
- Around line 39-43: The native export block (isNativeBuild) currently sets
output: 'export' but leaves server-only routing functions active; update the
config to guard rewrites(), redirects(), and headers() so they are only included
when !isNativeBuild (or move all server-routing logic into a dedicated native
config file), e.g., wrap the rewrites, redirects, and headers blocks behind the
same isNativeBuild check that sets output: 'export' so these functions are
skipped in native/export mode.
In `@next.config.native.js`:
- Around line 1-73: Delete the unused next.config.native.js file (it’s dead
code) and ensure the native build configuration remains only in next.config.js
which already checks NATIVE_BUILD/environment and applies the native options
(e.g., output: 'export', trailingSlash: true, images.unoptimized). Remove any
duplicate configuration in the repo so native-specific settings live solely in
next.config.js and do not conflict with or duplicate symbols found in
next.config.native.js (like transpilePackages,
experimental.optimizePackageImports, or webpack ignoreWarnings).
In `@scripts/native-build.js`:
- Around line 208-212: The execSync invocation in scripts/native-build.js
currently prefixes the command with the shell-specific token
"NATIVE_BUILD=true", which breaks on Windows; remove that prefix so the command
string is just "next build --webpack" and rely on the env object already passing
NATIVE_BUILD: 'true' (update the call where execSync('NATIVE_BUILD=true next
build --webpack', { ... }) is used), ensuring stdio/cwd/env remain unchanged so
the build runs cross-platform.
In `@src/app/`(mobile-ui)/native-poc/page.tsx:
- Around line 595-689: The testZeroDevTransaction handler currently always
constructs a real kernelClient that uses BUNDLER_URL and PAYMASTER_URL (via
createZeroDevPaymasterClient) and can spend sponsor budget; add an explicit
non-production guard (e.g., check a feature flag like
NEXT_PUBLIC_ENABLE_SPONSORED_ARBITRUM or a runtime boolean) at the top of
testZeroDevTransaction and abort early (setError and return) when the flag is
false, or alternatively fall back to a non-sponsored flow by constructing
kernelClient without the paymaster option; specifically gate the code paths that
reference BUNDLER_URL, PAYMASTER_URL, createZeroDevPaymasterClient, and the
kernelClient creation so sponsored transactions cannot run unless the explicit
flag is enabled.
- Around line 323-326: Replace the manual atob/replace decoding for
clientDataJSON with the existing base64URLToBytes utility to handle base64url
padding and decoding consistently: call base64URLToBytes on
cred.response.clientDataJSON and then decode the resulting Uint8Array to a
string (e.g., via new TextDecoder().decode(...)) wherever clientDataJSON is
used; keep using uint8ArrayToHexString for authenticatorData as before and
ensure you reference base64URLToBytes and clientDataJSON in the updated code.
In `@src/constants/routes.ts`:
- Line 71: The PUBLIC_ROUTES_REGEX includes "/native-poc" but the canonical
route arrays do not, causing isReservedRoute('/native-poc') to return false;
update the route constants so the slug is present in the same canonical lists —
add "native-poc" to DEDICATED_ROUTES and RESERVED_ROUTES (or move it into the
dev-only list if it should be internal) in src/constants/routes.ts so
PUBLIC_ROUTES_REGEX and the array-based checks (e.g., DEDICATED_ROUTES,
RESERVED_ROUTES, isReservedRoute) remain consistent.
In `@src/hooks/usePasskeySupport.ts`:
- Around line 33-38: In usePasskeySupport, the temporary bypass currently
unconditionally sets setBrowserSupported(true),
setConditionalMediationSupported(true), setIsSupported(true) and returns; change
this so the bypass only runs when the native build flag is present (e.g. check
the project’s native indicator like isNativeBuild / process.env.NATIVE_BUILD /
__ANDROID_POC__), and otherwise fall through to the existing web/HTTPS detection
logic; ensure the guarded branch references the same setters
(setBrowserSupported, setConditionalMediationSupported, setIsSupported) and
retains the early return only when the native flag is true.
In `@src/utils/passkeyPreflight.ts`:
- Around line 31-41: The temporary unconditional bypass in passkeyPreflight.ts
is returning isSupported=true and hardcoding diagnostics.isAndroid=true for all
builds; change it to only short-circuit when NEXT_PUBLIC_IS_NATIVE_BUILD (or a
dedicated debug flag) is truthy, otherwise run the real diagnostics path and
return the actual diagnostics/warning values; specifically wrap the current
early return so it only executes when the env flag is set, ensure diagnostics,
isSupported and warning values are preserved/derived from the existing checks
(e.g., the diagnostics object and any navigator.credentials checks) for
non-native builds.
---
Outside diff comments:
In `@src/components/Home/HomePerkClaimSection.tsx`:
- Around line 52-66: The file fails Prettier formatting; run your project
formatter (e.g., prettier --write) on HomePerkClaimSection.tsx or reformat the
useWebSocket call and its callback so it matches project style (ensure
consistent indentation, spacing and trailing commas) around useWebSocket,
useCallback, queryClient.invalidateQueries, setArrivalSource and
setArrivalPhase; then re-run CI to confirm the Prettier warning is resolved.
---
Nitpick comments:
In `@android/app/src/main/res/layout/activity_main.xml`:
- Around line 1-12: The activity_main.xml layout includes a WebView that is
likely unused because Capacitor's BridgeActivity manages its WebView
programmatically; remove or archive this boilerplate layout to avoid
confusion—specifically delete or move
android/app/src/main/res/layout/activity_main.xml (the <WebView> entry and
surrounding CoordinatorLayout) or add a comment indicating it is unused, and
ensure no references to activity_main.xml remain in AndroidManifest or
MainActivity so BridgeActivity continues to supply the WebView.
In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java`:
- Around line 1-17: The current unit test is the default Capacitor template
(package com.getcapacitor.myapp, class ExampleUnitTest, method
addition_isCorrect) and should be removed or replaced; either delete this test
file entirely or rename ExampleUnitTest and replace addition_isCorrect with
meaningful Android integration/unit tests relevant to our app (or add a TODO and
a real test scaffolding) so the placeholder 2+2==4 test does not clutter CI or
mask regressions.
In `@public/.well-known/assetlinks.json`:
- Around line 1-10: The assetlinks.json currently contains a debug signing
fingerprint in the sha256_cert_fingerprints array which must be replaced for
production; update the value in public/.well-known/assetlinks.json so the
"sha256_cert_fingerprints" entry under the "target" for package_name
"com.peanut.app" uses the release keystore SHA-256 fingerprint (or modify your
build to inject the correct fingerprint), and optionally add a brief comment or
README note near assetlinks.json indicating fingerprints must be
replaced/automatically injected for production releases.
In `@scripts/native-build.js`:
- Around line 194-224: The script mutates the worktree in-place via
disableItems() and wrapClientComponents() without handling interruptions; trap
SIGINT and SIGTERM in main so that on signal you call restoreModifiedFiles() and
restoreItems() (and set process.exitCode) before exiting, or alternatively
perform the file rewrites in a temporary copy and run the build there and always
call restoreModifiedFiles()/restoreItems() in a finally/cleanup handler; update
main to register signal handlers that reference
disableItems/wrapClientComponents/restoreModifiedFiles/restoreItems (and remove
handlers after completion) to ensure the repo is restored on interrupt.
In `@src/app/`(mobile-ui)/native-poc/page.tsx:
- Around line 719-722: The debug output container for testResult collapses
newlines; update the JSX element that renders testResult (the div that reads
{testResult && (...)} and uses classes including rounded p-2 text-sm break-all)
to preserve multiline output by adding the whitespace-pre-wrap utility (e.g.,
include "whitespace-pre-wrap" in the className list alongside or instead of
break-all) so \n in testResult renders as line breaks.
In `@src/app/page.tsx`:
- Around line 197-204: The temporary Native PoC Link JSX in page.tsx is rendered
unconditionally on the landing page; wrap the <Link ...> block in an environment
check so it only renders in non-production or when an explicit opt-in flag is
set (e.g., process.env.NODE_ENV !== 'production' or
process.env.NEXT_PUBLIC_ENABLE_NATIVE_POC === 'true'). Update the conditional
where the Link component appears to gate rendering, and ensure you use a
NEXT_PUBLIC_ prefixed env var if the check runs client-side so the variable is
available in the browser; default to not rendering when the env var is missing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 140b92ad-832b-40d4-8d0c-e0c5283abb27
⛔ Files ignored due to path filters (28)
android/app/src/main/res/drawable-land-hdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-mdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xxxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-hdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-mdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xxxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/gradle/wrapper/gradle-wrapper.jaris excluded by!**/*.jarpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (41)
android/.gitignoreandroid/app/.gitignoreandroid/app/build.gradleandroid/app/capacitor.build.gradleandroid/app/proguard-rules.proandroid/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.javaandroid/app/src/main/AndroidManifest.xmlandroid/app/src/main/java/com/peanut/app/MainActivity.javaandroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xmlandroid/app/src/main/res/drawable/ic_launcher_background.xmlandroid/app/src/main/res/layout/activity_main.xmlandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xmlandroid/app/src/main/res/values/ic_launcher_background.xmlandroid/app/src/main/res/values/strings.xmlandroid/app/src/main/res/values/styles.xmlandroid/app/src/main/res/xml/file_paths.xmlandroid/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.javaandroid/build.gradleandroid/capacitor.settings.gradleandroid/gradle.propertiesandroid/gradle/wrapper/gradle-wrapper.propertiesandroid/gradlewandroid/gradlew.batandroid/settings.gradleandroid/variables.gradlecapacitor.config.tsnext.config.jsnext.config.native.jspackage.jsonpublic/.well-known/assetlinks.jsonscripts/native-build.jssrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/native-poc/page.tsxsrc/app/(setup)/setup/page.tsxsrc/app/lp/card/CardLandingPage.tsxsrc/app/page.tsxsrc/components/Home/HomePerkClaimSection.tsxsrc/constants/routes.tssrc/hooks/usePasskeySupport.tssrc/utils/passkeyPreflight.ts
| // Context of the app under test. | ||
| Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | ||
|
|
||
| assertEquals("com.getcapacitor.app", appContext.getPackageName()); |
There was a problem hiding this comment.
Fix the stale template package assertion.
This test still expects the default Capacitor package id, so it will fail once instrumentation runs against the new app id (com.peanut.app in this PR).
✅ Suggested assertion update
- assertEquals("com.getcapacitor.app", appContext.getPackageName());
+ assertEquals("com.peanut.app", appContext.getPackageName());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| assertEquals("com.getcapacitor.app", appContext.getPackageName()); | |
| assertEquals("com.peanut.app", appContext.getPackageName()); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
at line 24, The test ExampleInstrumentedTest contains a stale package assertion
using appContext.getPackageName(); update the expected literal from
"com.getcapacitor.app" to the actual app id "com.peanut.app" so the assertion in
the test method (in ExampleInstrumentedTest) reflects the new package name used
by the app.
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <application | ||
| android:allowBackup="true" |
There was a problem hiding this comment.
Disable Android backup for the passkey-enabled WebView wrapper.
Leaving android:allowBackup="true" means WebView-backed app data can be copied into device/cloud backups unless you add explicit exclusion rules. For this kind of auth-enabled container, that is a risky default. Flip this to false for the POC, or add fullBackupContent / dataExtractionRules that exclude auth state before this ships.
🔒 Safer default
- android:allowBackup="true"
+ android:allowBackup="false"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/AndroidManifest.xml` at line 5, Change the Android
manifest's backup setting for the passkey-enabled WebView wrapper by setting the
android:allowBackup attribute to false (replace android:allowBackup="true" with
android:allowBackup="false") in the AndroidManifest so app data is not included
in device/cloud backups; alternatively, add explicit fullBackupContent or
dataExtractionRules that exclude authentication state if you prefer to keep
allowBackup true.
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <external-path name="my_images" path="." /> | ||
| <cache-path name="my_cache_images" path="." /> | ||
| </paths> No newline at end of file |
There was a problem hiding this comment.
Overly permissive FileProvider paths.
Using path="." exposes the entire root of external storage and cache directories via FileProvider URIs. This is a security concern — any code with access to these URIs could read/write any file in those directories.
For POC this may be acceptable, but for production, scope paths to specific subdirectories the app actually needs to share.
🛡️ Recommended fix to scope paths
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
- <external-path name="my_images" path="." />
- <cache-path name="my_cache_images" path="." />
+ <external-path name="my_images" path="Pictures/" />
+ <cache-path name="my_cache_images" path="images/" />
</paths>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | |
| <external-path name="my_images" path="." /> | |
| <cache-path name="my_cache_images" path="." /> | |
| </paths> | |
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | |
| <external-path name="my_images" path="Pictures/" /> | |
| <cache-path name="my_cache_images" path="images/" /> | |
| </paths> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/res/xml/file_paths.xml` around lines 2 - 5, The
FileProvider paths in file_paths.xml are too permissive (external-path
name="my_images" path="." and cache-path name="my_cache_images"
path=".")—restrict them to app-specific subdirectories instead: create and use a
dedicated external media folder (e.g., Pictures/YourApp or
Android/data/<package>/files/Pictures) for my_images and a dedicated cache
subfolder for my_cache_images, then update the path attributes to those specific
relative paths and ensure your app creates/uses those directories when sharing
files; this limits FileProvider URIs to only the files your app actually needs
to expose.
| server: { | ||
| url: 'https://3ff1-146-70-189-102.ngrok-free.app', | ||
| cleartext: false, | ||
| }, |
There was a problem hiding this comment.
Hardcoded ngrok URL will break for other developers.
This URL is specific to one developer's ngrok session and will not work for anyone else. Consider using an environment variable or documenting that developers need to update this URL.
💡 Suggested approach using environment variable
+// Set CAPACITOR_DEV_URL env var to your ngrok URL for passkey testing
const config: CapacitorConfig = {
appId: 'com.peanut.app',
appName: 'Peanut',
webDir: 'out',
// Load from ngrok for passkey testing (needs HTTPS + assetlinks)
server: {
- url: 'https://3ff1-146-70-189-102.ngrok-free.app',
+ url: process.env.CAPACITOR_DEV_URL || 'https://your-ngrok-url.ngrok-free.app',
cleartext: false,
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@capacitor.config.ts` around lines 9 - 12, The hardcoded ngrok URL in
capacitor.config.ts (server.url) will break for other developers; change
server.url to read from an environment variable (e.g.,
process.env.CAPACITOR_SERVER_URL) with a sensible fallback (or omit the url
entirely for local dev), and update the project README to document how to set
CAPACITOR_SERVER_URL for development. Ensure the code that reads server.url
handles missing/empty env values gracefully so other devs aren’t forced to use
the original ngrok address.
| android: { | ||
| allowMixedContent: true, | ||
| webContentsDebuggingEnabled: true, | ||
| }, |
There was a problem hiding this comment.
Debug settings enabled — do not ship to production.webContentsDebuggingEnabled: true allows inspection and modification of WebView state via Chrome DevTools. Security guidelines recommend that "the debugging of web contents option is switched off in release builds." "This is a security liability and should not be enabled in production builds of apps."
Similarly, allowMixedContent: true permits loading HTTP resources in an HTTPS context, weakening transport security.
These settings are appropriate for development/POC but must be disabled or made conditional before any production release.
🛡️ Recommended: Make debug settings conditional
android: {
- allowMixedContent: true,
- webContentsDebuggingEnabled: true,
+ // WARNING: These settings are for development only
+ // Set to false or remove for production builds
+ allowMixedContent: process.env.NODE_ENV !== 'production',
+ webContentsDebuggingEnabled: process.env.NODE_ENV !== 'production',
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| android: { | |
| allowMixedContent: true, | |
| webContentsDebuggingEnabled: true, | |
| }, | |
| android: { | |
| allowMixedContent: process.env.NODE_ENV === 'development', | |
| webContentsDebuggingEnabled: process.env.NODE_ENV === 'development', | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@capacitor.config.ts` around lines 14 - 17, The Android webview debug and
mixed-content settings are enabled (android.allowMixedContent and
android.webContentsDebuggingEnabled) which must be disabled for production;
update the configuration to set these flags false by default and make them
conditional on a development flag (e.g., process.env.NODE_ENV === "development"
or an env var like DEBUG_WEBVIEW) so they remain true only in dev builds, or
ensure release builds explicitly set android.allowMixedContent = false and
android.webContentsDebuggingEnabled = false (adjust the exported capacitor
config or initialization logic that builds the android config object to apply
this conditional).
| const authenticatorData = base64URLToBytes(cred.response.authenticatorData) | ||
| const authenticatorDataHex = uint8ArrayToHexString(authenticatorData) | ||
|
|
||
| const clientDataJSON = atob(cred.response.clientDataJSON.replace(/-/g, '+').replace(/_/g, '/')) |
There was a problem hiding this comment.
Use the existing base64URLToBytes utility for consistent and correct decoding.
Line 323 correctly uses base64URLToBytes() for authenticatorData, but line 326 manually decodes clientDataJSON with atob() and character replacement. This is both inconsistent and fragile—WebAuthn responses commonly omit base64url padding, and atob() after simple character replacement may throw on those valid inputs. The file already defines base64URLToBytes (lines 65–73) which properly handles padding, so apply the same pattern here.
Suggested fix
- const clientDataJSON = atob(cred.response.clientDataJSON.replace(/-/g, '+').replace(/_/g, '/'))
+ const clientDataJSON = new TextDecoder().decode(base64URLToBytes(cred.response.clientDataJSON))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const authenticatorData = base64URLToBytes(cred.response.authenticatorData) | |
| const authenticatorDataHex = uint8ArrayToHexString(authenticatorData) | |
| const clientDataJSON = atob(cred.response.clientDataJSON.replace(/-/g, '+').replace(/_/g, '/')) | |
| const authenticatorData = base64URLToBytes(cred.response.authenticatorData) | |
| const authenticatorDataHex = uint8ArrayToHexString(authenticatorData) | |
| const clientDataJSON = new TextDecoder().decode(base64URLToBytes(cred.response.clientDataJSON)) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(mobile-ui)/native-poc/page.tsx around lines 323 - 326, Replace the
manual atob/replace decoding for clientDataJSON with the existing
base64URLToBytes utility to handle base64url padding and decoding consistently:
call base64URLToBytes on cred.response.clientDataJSON and then decode the
resulting Uint8Array to a string (e.g., via new TextDecoder().decode(...))
wherever clientDataJSON is used; keep using uint8ArrayToHexString for
authenticatorData as before and ensure you reference base64URLToBytes and
clientDataJSON in the updated code.
| // Test actual ZeroDev transaction - sends 0 ETH to self | ||
| const testZeroDevTransaction = async () => { | ||
| if (!webAuthnKey) { | ||
| setError('No WebAuthnKey available. Create a passkey first!') | ||
| return | ||
| } | ||
|
|
||
| setIsLoading(true) | ||
| setError(null) | ||
| setTestResult(null) | ||
|
|
||
| try { | ||
| setTestResult('Creating ZeroDev kernel account...') | ||
|
|
||
| // Create public client for Arbitrum | ||
| const publicClient = createPublicClient({ | ||
| chain: arbitrum, | ||
| transport: http(), | ||
| }) | ||
|
|
||
| // Create passkey validator from our WebAuthnKey | ||
| const passkeyValidator = await toPasskeyValidator(publicClient, { | ||
| webAuthnKey, | ||
| entryPoint: USER_OP_ENTRY_POINT, | ||
| kernelVersion: ZERODEV_KERNEL_VERSION, | ||
| validatorContractVersion: PasskeyValidatorContractVersion.V0_0_3_PATCHED, | ||
| }) | ||
|
|
||
| setTestResult('Creating kernel account...') | ||
|
|
||
| // Create kernel account | ||
| const kernelAccount = await createKernelAccount(publicClient, { | ||
| plugins: { | ||
| sudo: passkeyValidator, | ||
| }, | ||
| entryPoint: USER_OP_ENTRY_POINT, | ||
| kernelVersion: ZERODEV_KERNEL_VERSION, | ||
| }) | ||
|
|
||
| const accountAddress = kernelAccount.address | ||
| setSmartAccountAddress(accountAddress) | ||
|
|
||
| setTestResult(`Smart Account Address: ${accountAddress}\n\nCreating kernel client...`) | ||
|
|
||
| // Create kernel client with paymaster | ||
| const kernelClient = createKernelAccountClient({ | ||
| account: kernelAccount, | ||
| chain: arbitrum, | ||
| bundlerTransport: http(BUNDLER_URL), | ||
| paymaster: { | ||
| getPaymasterData: async (userOperation) => { | ||
| const zerodevPaymaster = createZeroDevPaymasterClient({ | ||
| chain: arbitrum, | ||
| transport: http(PAYMASTER_URL), | ||
| }) | ||
| return await zerodevPaymaster.sponsorUserOperation({ | ||
| userOperation, | ||
| shouldOverrideFee: true, | ||
| }) | ||
| }, | ||
| }, | ||
| }) | ||
|
|
||
| setTestResult(`Smart Account: ${accountAddress}\n\nSending test transaction (0 ETH to self)...\n\n🔐 A passkey prompt should appear!`) | ||
|
|
||
| // Send a 0-value transaction to self (cheapest possible test) | ||
| const userOpHash = await kernelClient.sendUserOperation({ | ||
| callData: await kernelAccount.encodeCalls([ | ||
| { | ||
| to: accountAddress, | ||
| value: BigInt(0), | ||
| data: '0x', | ||
| }, | ||
| ]), | ||
| }) | ||
|
|
||
| setTestResult(`Smart Account: ${accountAddress}\n\nUserOp Hash: ${userOpHash}\n\nWaiting for transaction receipt...`) | ||
|
|
||
| // Wait for the transaction | ||
| const receipt = await kernelClient.waitForUserOperationReceipt({ | ||
| hash: userOpHash, | ||
| }) | ||
|
|
||
| const txHash = receipt.receipt.transactionHash | ||
| const arbiscanUrl = `https://arbiscan.io/tx/${txHash}` | ||
|
|
||
| setTestResult(`🎉 SUCCESS! On-chain transaction confirmed!\n\nSmart Account: ${accountAddress}\n\nUserOp Hash: ${userOpHash}\n\nTx Hash: ${txHash}\n\n🔗 View on Arbiscan:\n${arbiscanUrl}`) | ||
| } catch (err: any) { | ||
| console.error('ZeroDev transaction failed:', err) | ||
| setError(err.message || 'Unknown error') | ||
| setTestResult(`❌ ZeroDev FAILED: ${err.name || 'Error'} - ${err.message}`) | ||
| } finally { | ||
| setIsLoading(false) | ||
| } | ||
| } |
There was a problem hiding this comment.
Gate the sponsored Arbitrum path behind an explicit non-production flag.
This handler creates a real kernel client and spends sponsor budget through BUNDLER_URL / PAYMASTER_URL; cross-file, those constants are backed by public NEXT_PUBLIC_* envs. Even the 0 ETH self-transfer still burns paymaster gas, so shipping this page outside the demo environment gives anyone a way to drain that budget.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(mobile-ui)/native-poc/page.tsx around lines 595 - 689, The
testZeroDevTransaction handler currently always constructs a real kernelClient
that uses BUNDLER_URL and PAYMASTER_URL (via createZeroDevPaymasterClient) and
can spend sponsor budget; add an explicit non-production guard (e.g., check a
feature flag like NEXT_PUBLIC_ENABLE_SPONSORED_ARBITRUM or a runtime boolean) at
the top of testZeroDevTransaction and abort early (setError and return) when the
flag is false, or alternatively fall back to a non-sponsored flow by
constructing kernelClient without the paymaster option; specifically gate the
code paths that reference BUNDLER_URL, PAYMASTER_URL,
createZeroDevPaymasterClient, and the kernelClient creation so sponsored
transactions cannot run unless the explicit flag is enabled.
| * Exception: /dev/payment-graph is public (uses API key instead of user auth) | ||
| */ | ||
| export const PUBLIC_ROUTES_REGEX = /^\/(request\/pay|claim|pay\/.+|support|invite|qr|dev\/payment-graph)/ | ||
| export const PUBLIC_ROUTES_REGEX = /^\/(request\/pay|claim|pay\/.+|support|invite|qr|dev\/payment-graph|native-poc)/ |
There was a problem hiding this comment.
Keep /native-poc in the canonical route lists too.
Only the regex knows about this page right now. DEDICATED_ROUTES/RESERVED_ROUTES still exclude native-poc, so isReservedRoute('/native-poc') stays false and shared catch-all logic can still misclassify it. Add the slug to the dedicated/public route arrays as well, or move it to the dev-only list if this page should stay internal.
Minimal sync-up
export const DEDICATED_ROUTES = [
'qr',
'api',
'setup',
'home',
'history',
'settings',
'points',
'claim',
'pay',
'request',
'invite',
'support',
'dev',
+ 'native-poc',
] as const
@@
-export const PUBLIC_ROUTES = ['request/pay', 'claim', 'pay', 'support', 'invite', 'qr', 'dev/payment-graph'] as const
+export const PUBLIC_ROUTES = ['request/pay', 'claim', 'pay', 'support', 'invite', 'qr', 'dev/payment-graph', 'native-poc'] as const🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/constants/routes.ts` at line 71, The PUBLIC_ROUTES_REGEX includes
"/native-poc" but the canonical route arrays do not, causing
isReservedRoute('/native-poc') to return false; update the route constants so
the slug is present in the same canonical lists — add "native-poc" to
DEDICATED_ROUTES and RESERVED_ROUTES (or move it into the dev-only list if it
should be internal) in src/constants/routes.ts so PUBLIC_ROUTES_REGEX and the
array-based checks (e.g., DEDICATED_ROUTES, RESERVED_ROUTES, isReservedRoute)
remain consistent.
| // TEMPORARY POC BYPASS: Always assume passkey support | ||
| // TODO: Remove this after native PoC testing | ||
| setBrowserSupported(true) | ||
| setConditionalMediationSupported(true) | ||
| setIsSupported(true) | ||
| return |
There was a problem hiding this comment.
Scope the bypass to native builds only.
This hook is shared outside the Android POC. Returning true unconditionally here makes unsupported browsers and non-HTTPS contexts look passkey-capable, so normal web flows can advance into navigator.credentials.* calls that fail later. Gate the bypass behind the native build flag instead of short-circuiting the hook globally.
Minimal guard
try {
- // TEMPORARY POC BYPASS: Always assume passkey support
- // TODO: Remove this after native PoC testing
- setBrowserSupported(true)
- setConditionalMediationSupported(true)
- setIsSupported(true)
- return
+ const isNativeBuild = process.env.NEXT_PUBLIC_IS_NATIVE_BUILD === 'true'
+ if (isNativeBuild) {
+ // TEMPORARY POC BYPASS: only for the native export
+ setBrowserSupported(true)
+ setConditionalMediationSupported(true)
+ setIsSupported(true)
+ return
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/hooks/usePasskeySupport.ts` around lines 33 - 38, In usePasskeySupport,
the temporary bypass currently unconditionally sets setBrowserSupported(true),
setConditionalMediationSupported(true), setIsSupported(true) and returns; change
this so the bypass only runs when the native build flag is present (e.g. check
the project’s native indicator like isNativeBuild / process.env.NATIVE_BUILD /
__ANDROID_POC__), and otherwise fall through to the existing web/HTTPS detection
logic; ensure the guarded branch references the same setters
(setBrowserSupported, setConditionalMediationSupported, setIsSupported) and
retains the early return only when the native flag is true.
| // TEMPORARY POC BYPASS: Always return supported | ||
| // TODO: Remove after native PoC testing | ||
| return { | ||
| isSupported: true, | ||
| warning: null, | ||
| diagnostics: { | ||
| hasPublicKeyCredential: true, | ||
| isHttps: true, | ||
| isAndroid: true, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Scope the bypass to native-only paths.
This short-circuit makes every caller look like a supported Android/HTTPS/WebAuthn environment and hardcodes isAndroid: true, so non-native sessions skip the warning path and can fail later when navigator.credentials.* runs. Keep the POC bypass behind NEXT_PUBLIC_IS_NATIVE_BUILD (or a dedicated debug flag) and preserve the real diagnostics for other builds.
🔧 Minimal guard for the POC bypass
export async function checkPasskeySupport(): Promise<PasskeyPreflightResult> {
- // TEMPORARY POC BYPASS: Always return supported
- // TODO: Remove after native PoC testing
- return {
- isSupported: true,
- warning: null,
- diagnostics: {
- hasPublicKeyCredential: true,
- isHttps: true,
- isAndroid: true,
- },
- }
+ if (process.env.NEXT_PUBLIC_IS_NATIVE_BUILD === 'true') {
+ return {
+ isSupported: true,
+ warning: null,
+ diagnostics: {
+ hasPublicKeyCredential: 'PublicKeyCredential' in window,
+ isHttps: window.location.protocol === 'https:' || window.location.hostname === 'localhost',
+ isAndroid: /android/i.test(navigator.userAgent),
+ },
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/passkeyPreflight.ts` around lines 31 - 41, The temporary
unconditional bypass in passkeyPreflight.ts is returning isSupported=true and
hardcoding diagnostics.isAndroid=true for all builds; change it to only
short-circuit when NEXT_PUBLIC_IS_NATIVE_BUILD (or a dedicated debug flag) is
truthy, otherwise run the real diagnostics path and return the actual
diagnostics/warning values; specifically wrap the current early return so it
only executes when the env flag is set, ensure diagnostics, isSupported and
warning values are preserved/derived from the existing checks (e.g., the
diagnostics object and any navigator.credentials checks) for non-native builds.
What this is
Proof-of-concept for wrapping the Peanut Next.js app as a native Android app using Capacitor.js 8. The primary goal is testing whether passkeys (WebAuthn) work inside a Capacitor WebView — since passkeys are our auth mechanism and the biggest unknown for going native.
Architecture overview
The approach uses
output: 'export'to produce a fully static build, then Capacitor serves it inside an Android WebView. During development, the WebView points at an ngrok URL for live testing with HTTPS (required for passkeyrpIdmatching).What's included
Capacitor setup
capacitor.config.ts— App config (com.peanut.app), points WebView at ngrok URL for devandroid/— Full Android project scaffold (Gradle, manifest, splash screens, icons)package.json— New scripts:build:native,cap:sync,cap:open:android,cap:run:android@capacitor/core,@capacitor/android,@capacitor/cli,capacitor-webauthnStatic export build pipeline
next.config.native.js— Separate Next.js config for native builds withoutput: 'export', unoptimized images, relative asset paths,NEXT_PUBLIC_IS_NATIVE_BUILDenv flagscripts/native-build.js— Build orchestrator that:[param]routes withgenerateStaticParams()stubsnext buildwith static exportNative POC test page (
/native-poc)src/app/(mobile-ui)/native-poc/page.tsx(848 lines) — Comprehensive diagnostic page that tests:navigator.credentials.create()withcom.peanut.apprpIdnavigator.credentials.get()Passkey support utilities
src/hooks/usePasskeySupport.ts— Hook for checking WebAuthn + conditional mediation support (has a POC bypass that always returnstrue)src/utils/passkeyPreflight.ts— Detailed preflight checks: HTTPS, Chrome version, Android version, UVPA, conditional mediation, device model extraction (also has POC bypass)Android Digital Asset Links
public/.well-known/assetlinks.json— Updated with app signing certificate fingerprint for passkey rpId associationMinor changes
next.config.js, route constants, home page, setup page to accommodate native context detectionHow to run
For passkey testing, the WebView needs HTTPS with a domain that matches
assetlinks.json. The config currently uses an ngrok tunnel for this — update the URL incapacitor.config.tsto your own ngrok instance.Key findings / notes
capacitor-webauthnplugin provides native passkey bridge as fallback/native-poc) has step-by-step diagnostics to debug any passkey/signing issues on real devicesKnown TODOs (not for this PR)
capacitor.config.tsusePasskeySupport.tsandpasskeyPreflight.ts