Fix Amber native login in APK: reliable Capacitor detection (v1.1.7)

Root cause: LoginDialog and useCurrentUser detected the native app via
window.Capacitor, which is unreliable inside the WebView (false at mount / not
attached in some builds). So the ZapStore APK fell into the web intent:// redirect
flow and never opened Amber ("Waiting for Amber…" hang).

Fix: add isNativeAmberAvailable() in capacitorAmberSigner.ts using the imported
@capacitor/core Capacitor + native plugin presence, and use it (evaluated at
click time) in LoginDialog.handleAmberLogin and in useCurrentUser's signer
selection. The APK now calls getAmberPublicKeyViaPlugin() → native intent → Amber
opens directly, no redirects.

Bump version to 1.1.7.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-08-27 14:00:39 +07:00
parent 3dea1e42dc
commit c6372742a9
4 changed files with 25 additions and 9 deletions

View file

@ -1,7 +1,7 @@
{
"name": "skate-spots",
"private": true,
"version": "1.1.6",
"version": "1.1.7",
"type": "module",
"homepage": "https://skate-spots.com",
"scripts": {

View file

@ -16,7 +16,7 @@ import { useLoginActions } from '@/hooks/useLoginActions';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
import { openAmberForLogin, subscribeToAmberReturn } from '@/lib/amber';
import { getAmberPublicKeyViaPlugin } from '@/lib/capacitorAmberSigner';
import { getAmberPublicKeyViaPlugin, isNativeAmberAvailable } from '@/lib/capacitorAmberSigner';
interface LoginDialogProps {
@ -45,9 +45,6 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
const isAndroid = /Android/i.test(navigator.userAgent);
// Detect if running inside a Capacitor native app
const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.();
// Step 1: open Amber
// - On Capacitor (ZapStore APK): use the native plugin — direct Promise, no redirects needed
// - On web/PWA: use intent:// URL with callbackUrl so Amber redirects back
@ -55,7 +52,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
setAmberStep('loading');
setAmberError('');
if (isCapacitor) {
// Decide native-vs-web at CLICK TIME using the reliable Capacitor check from
// the signer module (imported @capacitor/core), NOT window.Capacitor which
// can be false at mount inside the WebView — that mismatch was causing the
// APK to fall into the web redirect flow and never actually open Amber.
if (isNativeAmberAvailable()) {
// ── Native path (ZapStore/Capacitor APK) ────────────────────────────────
// Attempt login directly — skip isExternalSignerInstalled() check because
// Android 11+ package visibility rules can make it return false even when

View file

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import { useAuthor } from './useAuthor.ts';
import { AmberSigner } from '@/lib/amber';
import { CapacitorAmberSigner } from '@/lib/capacitorAmberSigner';
import { CapacitorAmberSigner, isNativeAmberAvailable } from '@/lib/capacitorAmberSigner';
import type { AmberLoginType } from './useLoginActions';
/** Extended login type that includes Amber */
@ -27,8 +27,8 @@ export function useCurrentUser() {
// On Capacitor (ZapStore APK) use the native plugin signer so all
// signing operations go through Amber via native intents.
// On web/PWA fall back to the URL-redirect based AmberSigner.
const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.();
const signer = isCapacitor
// Use the reliable module-level Capacitor check (not window.Capacitor).
const signer = isNativeAmberAvailable()
? new CapacitorAmberSigner(amberLogin.pubkey)
: new AmberSigner(amberLogin.pubkey);
return new NUser('x-amber', amberLogin.pubkey, signer);

View file

@ -257,6 +257,21 @@ export class CapacitorAmberSigner implements NostrSigner {
};
}
/**
* True when we're running inside the native Capacitor app AND the native
* NostrSigner bridge is usable. This is the ONLY reliable native check —
* reading `window.Capacitor` from a component can be false at mount time or in
* builds where Capacitor doesn't attach itself to `window`. Always call this at
* the moment of the action (e.g. inside the login click handler), not at render.
*/
export function isNativeAmberAvailable(): boolean {
try {
return !!Capacitor?.isNativePlatform?.() && !!getNativeSigner();
} catch {
return false;
}
}
/**
* Is an external signer (Amber) installed on this device?
*/