diff --git a/docker-compose.yml b/docker-compose.yml
index a114c15..c994d6b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -20,7 +20,7 @@ services:
context: .
dockerfile: Dockerfile
args:
- CACHEBUST: "16"
+ CACHEBUST: "17"
VITE_USE_MOCK_DATA: "false"
VITE_CONTENT_ORIGIN: ${FRONTEND_URL}
VITE_INDEEHUB_API_URL: /api
diff --git a/src/components/AuthModal.vue b/src/components/AuthModal.vue
index 0ca50e4..1b6250d 100644
--- a/src/components/AuthModal.vue
+++ b/src/components/AuthModal.vue
@@ -103,13 +103,14 @@
Sign in with Nostr Extension
-
+
+
+
+
{{ mode === 'login' ? "Don't have an account?" : "Already have an account?" }}
@@ -156,7 +181,7 @@ const props = withDefaults(defineProps
(), {
const emit = defineEmits()
const { login, loginWithNostr, register, isLoading: authLoading } = useAuth()
-const { loginWithExtension, loginWithAmber } = useAccounts()
+const { loginWithExtension } = useAccounts()
const mode = ref<'login' | 'register' | 'forgot'>(props.defaultMode)
const formData = ref({
@@ -167,11 +192,15 @@ const formData = ref({
const errorMessage = ref(null)
const isLoading = computed(() => authLoading.value)
+// Amber two-phase login state
+const amberPhase = ref<'idle' | 'waiting'>('idle')
+
function closeModal() {
emit('close')
- // Reset form
+ // Reset form and Amber state
formData.value = { email: '', password: '', legalName: '' }
errorMessage.value = null
+ amberPhase.value = 'idle'
}
function toggleMode() {
@@ -229,20 +258,74 @@ async function handleNostrLogin() {
}
/**
- * Login with Amber (NIP-55 Android Signer)
- * Uses the AmberClipboardSigner from applesauce-signers which
- * handles the Android intent flow and clipboard-based result reading.
- * Amber copies the pubkey to clipboard, and when the user returns
- * to the browser the signer reads it automatically.
+ * Amber Login — Phase 1: Open Amber via Android intent.
+ *
+ * Amber copies the pubkey to the clipboard. Mobile browsers block
+ * automatic clipboard reads (navigator.clipboard.readText requires a
+ * user gesture), so we move to phase 2 where the user taps a button
+ * to trigger the read inside a real user-gesture event handler.
*/
-async function handleAmberLogin() {
+function handleAmberOpen() {
+ errorMessage.value = null
+
+ // Check platform support
+ const isAndroid = navigator.userAgent.includes('Android')
+ if (!isAndroid) {
+ errorMessage.value = 'Amber is only available on Android devices. Please use a Nostr browser extension instead.'
+ return
+ }
+
+ // Open Amber via the nostrsigner intent
+ const intent = 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end'
+ window.open(intent, '_blank')
+
+ // Switch to phase 2 — user will tap "Complete Sign-in" after Amber approves
+ amberPhase.value = 'waiting'
+}
+
+/**
+ * Amber Login — Phase 2: Read the pubkey from the clipboard.
+ *
+ * This runs inside a click handler (user gesture) so
+ * navigator.clipboard.readText() is permitted by the browser.
+ */
+async function handleAmberComplete() {
errorMessage.value = null
try {
- // Get the pubkey from Amber (opens intent, reads clipboard on return)
- const pubkey = await loginWithAmber()
+ // Read pubkey from clipboard (allowed because this is a user gesture)
+ if (!navigator.clipboard?.readText) {
+ throw new Error('Clipboard API not available')
+ }
+ const clipboardText = (await navigator.clipboard.readText()).trim()
- // Create auth session with the pubkey from Amber
+ if (!clipboardText) {
+ throw new Error('Clipboard is empty. Please try again — make sure you approved the request in Amber.')
+ }
+
+ // Decode the pubkey (Amber may return hex, npub, or nprofile)
+ let pubkey: string
+ if (/^[0-9a-f]{64}$/i.test(clipboardText)) {
+ pubkey = clipboardText
+ } else if (clipboardText.startsWith('npub') || clipboardText.startsWith('nprofile')) {
+ // Decode using nostr-tools or applesauce helpers
+ const { decodeProfilePointer } = await import('applesauce-core/helpers')
+ const decoded = decodeProfilePointer(clipboardText)
+ if (!decoded?.pubkey) throw new Error('Could not decode npub from clipboard')
+ pubkey = decoded.pubkey
+ } else {
+ throw new Error('Clipboard does not contain a valid Nostr public key. Got: ' + clipboardText.slice(0, 20) + '...')
+ }
+
+ // Register the Amber account in the account manager
+ const { AmberClipboardSigner, AmberClipboardAccount, accountManager } = await import('../lib/accounts')
+ const signer = new AmberClipboardSigner()
+ signer.pubkey = pubkey
+ const account = new AmberClipboardAccount(pubkey, signer)
+ accountManager.addAccount(account)
+ accountManager.setActive(account)
+
+ // Create backend auth session
await loginWithNostr(pubkey, 'amber-nip55', {
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
@@ -254,17 +337,20 @@ async function handleAmberLogin() {
pubkey,
})
+ amberPhase.value = 'idle'
emit('success')
closeModal()
} catch (error: any) {
- console.error('Amber login failed:', error)
+ console.error('Amber clipboard read failed:', error)
- if (error.message?.includes('non-Android')) {
- errorMessage.value = 'Amber is only available on Android devices. Please use a Nostr browser extension instead.'
- } else if (error.message?.includes('clipboard') || error.message?.includes('Empty')) {
- errorMessage.value = 'Could not read from clipboard. Please ensure Amber is installed and clipboard permissions are granted.'
+ if (error.message?.includes('Clipboard is empty')) {
+ errorMessage.value = 'Clipboard is empty. Open Amber, approve the request, then come back and tap "Complete Sign-in".'
+ } else if (error.message?.includes('not available')) {
+ errorMessage.value = 'Clipboard access is not available. Try using Chrome on Android.'
+ } else if (error.message?.includes('permission')) {
+ errorMessage.value = 'Clipboard permission denied. Please allow clipboard access and try again.'
} else {
- errorMessage.value = error.message || 'Amber login failed. Please try again.'
+ errorMessage.value = error.message || 'Failed to read from Amber. Please try again.'
}
}
}
@@ -375,6 +461,17 @@ declare global {
box-shadow: 0 0 20px rgba(247, 147, 26, 0.1);
}
+.amber-login-button--active {
+ background: rgba(247, 147, 26, 0.15);
+ border-color: rgba(247, 147, 26, 0.5);
+ box-shadow: 0 0 24px rgba(247, 147, 26, 0.15);
+}
+
+.amber-login-button--active:hover {
+ background: rgba(247, 147, 26, 0.22);
+ box-shadow: 0 0 30px rgba(247, 147, 26, 0.2);
+}
+
.amber-login-button:disabled {
opacity: 0.5;
cursor: not-allowed;