Amber signing: add timeout + one-time setPackageName so hangs surface as errors (v1.0.74)
Symptom: crew logo upload spins forever, and crew creation appears to do nothing. Both go through Amber signEvent. Login (getPublicKey) works because it uses Amber's silent content-resolver path, but signEvent must launch Amber's approval Activity and wait for the result — and if that result never comes back (buggy plugin build, user backs out, missing permission) the bridge promise never settles, so the UI hangs with no error. Changes: - withTimeout() wraps every Amber bridge call (sign/getPublicKey/nip04/nip44) with a 90s cap. A hung round-trip now rejects with a clear, actionable message instead of spinning forever, which also unblocks the CrewForm (isUploading/isPublishing finally resolve). - setPackageName is now called ONCE per session (cached promise) instead of before every op — repeated calls are needless bridge chatter and can wedge on some builds. NOTE (root cause, fixed on the build machine): package.json requests the GitHub `main` build of nostr-signer-capacitor-plugin, but package-lock.json still pins the old npm 0.0.5 (which the APK bundles). 0.0.5 is the version prone to the signEvent activity-result hang. Regenerate the lockfile with `npm install` so the `main` build is used — see chat for steps. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
b3a025ca77
commit
04b8b0a8b1
2 changed files with 110 additions and 42 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "skate-spots",
|
"name": "skate-spots",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.73",
|
"version": "1.0.74",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"homepage": "https://skate-spots.com",
|
"homepage": "https://skate-spots.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,42 @@ function makeId(): string {
|
||||||
return `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
return `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How long to wait for Amber to return before giving up. Amber is a separate
|
||||||
|
// app: the user has to switch to it, approve, and switch back. If the native
|
||||||
|
// activity-result never fires (user backs out, Amber not granted, etc.) the
|
||||||
|
// bridge promise would otherwise hang FOREVER — which is exactly the "stuck
|
||||||
|
// loading" symptom on the crew logo upload and crew publish. This turns that
|
||||||
|
// silent hang into a clear, catchable error.
|
||||||
|
const AMBER_TIMEOUT_MS = 90_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Race a bridge promise against a timeout so a hung Amber round-trip surfaces
|
||||||
|
* as an error instead of spinning forever.
|
||||||
|
*/
|
||||||
|
function withTimeout<T>(promise: Promise<T>, label: string): Promise<T> {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Amber didn't respond to the ${label} request. Open Amber, approve the request, then try again. ` +
|
||||||
|
`If nothing appeared, make sure Amber is installed and SkateSpots is allowed to use it.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, AMBER_TIMEOUT_MS);
|
||||||
|
|
||||||
|
promise.then(
|
||||||
|
(value) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Lazily build the native bridge proxy once. `registerPlugin` is a pure factory
|
// Lazily build the native bridge proxy once. `registerPlugin` is a pure factory
|
||||||
// (builds a proxy, touches no native APIs at call time), so it's safe on web too
|
// (builds a proxy, touches no native APIs at call time), so it's safe on web too
|
||||||
// — the proxy simply has no implementation there and calls reject.
|
// — the proxy simply has no implementation there and calls reject.
|
||||||
|
|
@ -65,6 +101,25 @@ function getNativeSigner(): NostrSignerNative | null {
|
||||||
return nativePlugin;
|
return nativePlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the Amber package name exactly ONCE per app session. Calling it before
|
||||||
|
// every sign/encrypt op is unnecessary bridge chatter and, on some builds, a
|
||||||
|
// repeated setPackageName can itself wedge. We cache the promise so concurrent
|
||||||
|
// callers await the same setup.
|
||||||
|
let packageNameSet: Promise<void> | null = null;
|
||||||
|
function ensurePackageName(plugin: NostrSignerNative): Promise<void> {
|
||||||
|
if (!packageNameSet) {
|
||||||
|
packageNameSet = plugin
|
||||||
|
.setPackageName({ packageName: AMBER_PACKAGE })
|
||||||
|
.catch((e) => {
|
||||||
|
// setPackageName can be a no-op that throws on some builds; log but
|
||||||
|
// don't block signing, and don't cache the failure.
|
||||||
|
console.warn('[CapacitorAmberSigner] setPackageName warning:', e);
|
||||||
|
packageNameSet = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return packageNameSet;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the native signer, ensuring the Amber package name is set first.
|
* Get the native signer, ensuring the Amber package name is set first.
|
||||||
* Throws a clear error if not on a native platform (caller can surface it).
|
* Throws a clear error if not on a native platform (caller can surface it).
|
||||||
|
|
@ -74,12 +129,7 @@ async function getPlugin(): Promise<NostrSignerNative> {
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
throw new Error('Amber signing is only available in the SkateSpots Android app.');
|
throw new Error('Amber signing is only available in the SkateSpots Android app.');
|
||||||
}
|
}
|
||||||
try {
|
await ensurePackageName(plugin);
|
||||||
await plugin.setPackageName({ packageName: AMBER_PACKAGE });
|
|
||||||
} catch (e) {
|
|
||||||
// setPackageName can be a no-op that throws on some builds; log but continue.
|
|
||||||
console.warn('[CapacitorAmberSigner] setPackageName warning:', e);
|
|
||||||
}
|
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +143,10 @@ async function getPlugin(): Promise<NostrSignerNative> {
|
||||||
export async function getAmberPublicKeyViaPlugin(): Promise<string> {
|
export async function getAmberPublicKeyViaPlugin(): Promise<string> {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
|
|
||||||
const result = await plugin.getPublicKey({ packageName: AMBER_PACKAGE });
|
const result = await withTimeout(
|
||||||
|
plugin.getPublicKey({ packageName: AMBER_PACKAGE }),
|
||||||
|
'login',
|
||||||
|
);
|
||||||
|
|
||||||
// JSObject from Capacitor — normalize to a plain object for reliable access.
|
// JSObject from Capacitor — normalize to a plain object for reliable access.
|
||||||
const plain = JSON.parse(JSON.stringify(result)) as Record<string, string>;
|
const plain = JSON.parse(JSON.stringify(result)) as Record<string, string>;
|
||||||
|
|
@ -129,36 +182,45 @@ export class CapacitorAmberSigner implements NostrSigner {
|
||||||
|
|
||||||
async signEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
|
async signEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
const { event: signedJson } = await plugin.signEvent({
|
const { event: signedJson } = await withTimeout(
|
||||||
packageName: AMBER_PACKAGE,
|
plugin.signEvent({
|
||||||
eventJson: JSON.stringify(event),
|
packageName: AMBER_PACKAGE,
|
||||||
eventId: makeId(),
|
eventJson: JSON.stringify(event),
|
||||||
npub: this.npub,
|
eventId: makeId(),
|
||||||
});
|
npub: this.npub,
|
||||||
|
}),
|
||||||
|
'signature',
|
||||||
|
);
|
||||||
return JSON.parse(signedJson) as NostrEvent;
|
return JSON.parse(signedJson) as NostrEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly nip04 = {
|
readonly nip04 = {
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
const { result } = await plugin.nip04Encrypt({
|
const { result } = await withTimeout(
|
||||||
packageName: AMBER_PACKAGE,
|
plugin.nip04Encrypt({
|
||||||
plainText: plaintext,
|
packageName: AMBER_PACKAGE,
|
||||||
pubKey: pubkey,
|
plainText: plaintext,
|
||||||
npub: this.npub,
|
pubKey: pubkey,
|
||||||
id: makeId(),
|
npub: this.npub,
|
||||||
});
|
id: makeId(),
|
||||||
|
}),
|
||||||
|
'encryption',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
const { result } = await plugin.nip04Decrypt({
|
const { result } = await withTimeout(
|
||||||
packageName: AMBER_PACKAGE,
|
plugin.nip04Decrypt({
|
||||||
encryptedText: ciphertext,
|
packageName: AMBER_PACKAGE,
|
||||||
pubKey: pubkey,
|
encryptedText: ciphertext,
|
||||||
npub: this.npub,
|
pubKey: pubkey,
|
||||||
id: makeId(),
|
npub: this.npub,
|
||||||
});
|
id: makeId(),
|
||||||
|
}),
|
||||||
|
'decryption',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -166,24 +228,30 @@ export class CapacitorAmberSigner implements NostrSigner {
|
||||||
readonly nip44 = {
|
readonly nip44 = {
|
||||||
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
encrypt: async (pubkey: string, plaintext: string): Promise<string> => {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
const { result } = await plugin.nip44Encrypt({
|
const { result } = await withTimeout(
|
||||||
packageName: AMBER_PACKAGE,
|
plugin.nip44Encrypt({
|
||||||
plainText: plaintext,
|
packageName: AMBER_PACKAGE,
|
||||||
pubKey: pubkey,
|
plainText: plaintext,
|
||||||
npub: this.npub,
|
pubKey: pubkey,
|
||||||
id: makeId(),
|
npub: this.npub,
|
||||||
});
|
id: makeId(),
|
||||||
|
}),
|
||||||
|
'encryption',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
decrypt: async (pubkey: string, ciphertext: string): Promise<string> => {
|
||||||
const plugin = await getPlugin();
|
const plugin = await getPlugin();
|
||||||
const { result } = await plugin.nip44Decrypt({
|
const { result } = await withTimeout(
|
||||||
packageName: AMBER_PACKAGE,
|
plugin.nip44Decrypt({
|
||||||
encryptedText: ciphertext,
|
packageName: AMBER_PACKAGE,
|
||||||
pubKey: pubkey,
|
encryptedText: ciphertext,
|
||||||
npub: this.npub,
|
pubKey: pubkey,
|
||||||
id: makeId(),
|
npub: this.npub,
|
||||||
});
|
id: makeId(),
|
||||||
|
}),
|
||||||
|
'decryption',
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue