Root cause: the stock nostr-signer-capacitor-plugin (0.0.5) tries Amber's
background ContentProvider first, which GrapheneOS blocks and never returns,
with no clean fallback — so signing hung 90s and Amber never appeared.
Fix (matches Amethyst, the reference NIP-55 client):
- New native Capacitor plugin AmberIntent (android-templates/AmberIntentPlugin.java)
that goes STRAIGHT to a foreground nostrsigner: intent via a properly-registered
ActivityResultLauncher, tracks each request by id, and times out gracefully.
- MainActivity now registers the launcher in onCreate (AndroidX contract) and
hands it to the plugin, while staying a safe stock BridgeActivity (no
WebViewClient override → no black screen).
- inject-main-activity.mjs now installs both MainActivity.java and AmberIntentPlugin.java.
- capacitorAmberSigner.ts rewired to registerPlugin('AmberIntent'); every op is a
user-visible Amber Approve/Reject popup; rejections surface as a friendly message.
- Removed the dead npm nostr-signer-capacitor-plugin dependency (and its phantom
coracle fork spec) plus its vite external entry — signing no longer uses it.
- Delete flow copy updated to the new foreground UX (Approve in Amber / Cancelled
on reject) instead of the diagnostic SIGN/PUBLISH narration.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
import { isAdminPubkey } from '@/lib/admins';
|
|
import { addLocalHidden } from '@/lib/localModeration';
|
|
import { toast } from '@/hooks/useToast';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
/**
|
|
* DIAGNOSTIC DELETE
|
|
* =================
|
|
* We could not identify WHERE the delete hangs (signing? relay publish?), so
|
|
* this version narrates every step on-screen via toasts and splits the flow into
|
|
* explicit, independently-timed phases:
|
|
*
|
|
* 1. SIGN — call the signer (Amber). Own timeout. If it hangs here, Amber
|
|
* never returned (app↔Amber problem).
|
|
* 2. PUBLISH — send the signed event to relays. Own timeout. If it hangs here,
|
|
* the relay is the problem, not Amber.
|
|
*
|
|
* Whatever step is on screen when it freezes tells us EXACTLY what to fix.
|
|
*/
|
|
|
|
const SIGN_TIMEOUT_MS = 90_000; // matches the signer's own cap
|
|
const PUBLISH_TIMEOUT_MS = 20_000; // relays should answer well within this
|
|
|
|
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s.`));
|
|
}, ms);
|
|
promise.then(
|
|
(v) => {
|
|
clearTimeout(timer);
|
|
resolve(v);
|
|
},
|
|
(e) => {
|
|
clearTimeout(timer);
|
|
reject(e);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
export function useDeleteEntity() {
|
|
const { nostr } = useNostr();
|
|
const { user } = useCurrentUser();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: ({
|
|
event,
|
|
reason,
|
|
}: {
|
|
event: NostrEvent;
|
|
reason?: string;
|
|
}) => runDelete(event, reason),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries();
|
|
},
|
|
});
|
|
|
|
async function runDelete(event: NostrEvent, reason?: string): Promise<void> {
|
|
if (!user) throw new Error('You must be logged in to delete.');
|
|
|
|
const isOwner = user.pubkey === event.pubkey;
|
|
const isAdmin = isAdminPubkey(user.pubkey);
|
|
if (!isOwner && !isAdmin) {
|
|
throw new Error('You can only delete your own listing.');
|
|
}
|
|
|
|
const dTag = event.tags.find(([name]) => name === 'd')?.[1] ?? '';
|
|
const coord = `${event.kind}:${event.pubkey}:${dTag}`;
|
|
|
|
const deletionTags: string[][] = [
|
|
['a', coord],
|
|
['e', event.id],
|
|
['k', String(event.kind)],
|
|
];
|
|
|
|
const template = {
|
|
kind: 5,
|
|
content: reason?.trim() || '',
|
|
tags: deletionTags,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
};
|
|
|
|
// ── PHASE 1: SIGN ────────────────────────────────────────────────────────
|
|
// With the Amethyst-style foreground signer, Amber pops up its Approve/Reject
|
|
// screen directly — no more silent ContentProvider stall. The signer has its
|
|
// own graceful timeout, but we keep an outer guard as a belt-and-braces.
|
|
toast({
|
|
title: 'Approve in Amber',
|
|
description: 'Amber will open — tap Approve to confirm the deletion.',
|
|
});
|
|
|
|
if (!user.signer) {
|
|
throw new Error('No signer available. Please log in again.');
|
|
}
|
|
|
|
let signed: NostrEvent;
|
|
try {
|
|
signed = await withTimeout(
|
|
user.signer.signEvent(template),
|
|
SIGN_TIMEOUT_MS,
|
|
'Signing (Amber)',
|
|
);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
// Distinguish a deliberate rejection from a real failure.
|
|
if (/reject/i.test(msg)) {
|
|
toast({ title: 'Cancelled', description: 'You rejected the request in Amber.' });
|
|
throw new Error('Signing cancelled.');
|
|
}
|
|
toast({
|
|
title: "Couldn't sign",
|
|
description: msg,
|
|
variant: 'destructive',
|
|
});
|
|
throw new Error(`SIGN step failed: ${msg}`);
|
|
}
|
|
|
|
// ── PHASE 2: PUBLISH ─────────────────────────────────────────────────────
|
|
toast({
|
|
title: 'Step 2/2: Publishing…',
|
|
description: 'Sending the deletion to relays.',
|
|
});
|
|
|
|
try {
|
|
await withTimeout(
|
|
nostr.event(signed),
|
|
PUBLISH_TIMEOUT_MS,
|
|
'Publishing (relays)',
|
|
);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
// The event was SIGNED fine — the relay is the slow/failing part. Hide it
|
|
// locally so the user's action still takes visible effect on this device.
|
|
if (isAdmin && !isOwner) addLocalHidden({ coord, id: event.id });
|
|
toast({
|
|
title: 'Stuck at PUBLISHING',
|
|
description: `Relay step failed: ${msg}. It was signed OK; the relay didn't confirm.`,
|
|
variant: 'destructive',
|
|
});
|
|
throw new Error(`PUBLISH step failed: ${msg}`);
|
|
}
|
|
|
|
// Admin: hide locally too (no signature needed).
|
|
if (isAdmin && !isOwner) {
|
|
addLocalHidden({ coord, id: event.id });
|
|
}
|
|
|
|
toast({ title: 'Deleted', description: 'The deletion was signed and published.' });
|
|
}
|
|
}
|