Browse Source

reinstate browser extension login on mobile

imwald
Silberengel 7 days ago
parent
commit
f2eff7fa8d
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 4
      src/components/AccountManager/index.tsx
  4. 34
      src/hooks/useNip07ExtensionAvailable.ts
  5. 14
      src/providers/NostrProvider/nip-07.signer.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.21.6", "version": "23.21.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.21.6", "version": "23.21.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.21.6", "version": "23.21.7",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

4
src/components/AccountManager/index.tsx

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { useNip07ExtensionAvailable } from '@/hooks/useNip07ExtensionAvailable'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { generateSecretKey } from 'nostr-tools' import { generateSecretKey } from 'nostr-tools'
import { nsecEncode } from 'nostr-tools/nip19' import { nsecEncode } from 'nostr-tools/nip19'
@ -44,6 +45,7 @@ function AccountManagerNav({
const { t } = useTranslation() const { t } = useTranslation()
const { nip07Login, nsecLogin, accounts, isNip07LoginInFlight, requestAccountNetworkHydrate } = const { nip07Login, nsecLogin, accounts, isNip07LoginInFlight, requestAccountNetworkHydrate } =
useNostr() useNostr()
const nip07ExtensionAvailable = useNip07ExtensionAvailable()
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [signingUp, setSigningUp] = useState(false) const [signingUp, setSigningUp] = useState(false)
const [extensionLoginPending, setExtensionLoginPending] = useState(false) const [extensionLoginPending, setExtensionLoginPending] = useState(false)
@ -84,7 +86,7 @@ function AccountManagerNav({
{t('Add an Account')} {t('Add an Account')}
</div> </div>
<div className="space-y-2 mt-4"> <div className="space-y-2 mt-4">
{!!window.nostr && ( {nip07ExtensionAvailable && (
<Button <Button
onClick={() => void handleExtensionLogin()} onClick={() => void handleExtensionLogin()}
disabled={extensionLoginPending || isNip07LoginInFlight} disabled={extensionLoginPending || isNip07LoginInFlight}

34
src/hooks/useNip07ExtensionAvailable.ts

@ -0,0 +1,34 @@
import {
NIP07_INJECT_CHECK_INTERVAL_MS,
NIP07_INJECT_MAX_ATTEMPTS
} from '@/providers/NostrProvider/nip-07.signer'
import { useEffect, useState } from 'react'
/**
* True once a NIP-07 browser extension has injected `window.nostr`.
* Polls briefly mobile browsers often expose the API after first paint.
*/
export function useNip07ExtensionAvailable(): boolean {
const [available, setAvailable] = useState(() => !!window.nostr)
useEffect(() => {
if (available) return
let attempt = 0
const id = window.setInterval(() => {
if (window.nostr) {
setAvailable(true)
window.clearInterval(id)
return
}
attempt += 1
if (attempt >= NIP07_INJECT_MAX_ATTEMPTS) {
window.clearInterval(id)
}
}, NIP07_INJECT_CHECK_INTERVAL_MS)
return () => window.clearInterval(id)
}, [available])
return available
}

14
src/providers/NostrProvider/nip-07.signer.ts

@ -1,6 +1,11 @@
import { pubkeyFromNip07Extension } from '@/lib/pubkey' import { pubkeyFromNip07Extension } from '@/lib/pubkey'
import { ISigner, TDraftEvent, TNip07 } from '@/types' import { ISigner, TDraftEvent, TNip07 } from '@/types'
/** Poll interval while waiting for a NIP-07 extension to inject `window.nostr`. */
export const NIP07_INJECT_CHECK_INTERVAL_MS = 100
/** Some mobile browsers inject the extension API well after first paint. */
export const NIP07_INJECT_MAX_ATTEMPTS = 120
/** Fresh extension pubkey (hex), after init + optional enable. */ /** Fresh extension pubkey (hex), after init + optional enable. */
export async function getExtensionPubkeyHex(): Promise<string> { export async function getExtensionPubkeyHex(): Promise<string> {
const signer = new Nip07Signer() const signer = new Nip07Signer()
@ -22,12 +27,7 @@ export class Nip07Signer implements ISigner {
private pubkey: string | null = null private pubkey: string | null = null
async init() { async init() {
const checkInterval = 100 for (let attempt = 0; attempt < NIP07_INJECT_MAX_ATTEMPTS; attempt++) {
// Some browser extensions inject `window.nostr` a bit later during startup/reload.
// Keep waiting longer to avoid false "no signer extension" failures on session restore.
const maxAttempts = 120
for (let attempt = 0; attempt < maxAttempts; attempt++) {
if (window.nostr) { if (window.nostr) {
this.signer = window.nostr this.signer = window.nostr
if (typeof this.signer.enable === 'function') { if (typeof this.signer.enable === 'function') {
@ -35,7 +35,7 @@ export class Nip07Signer implements ISigner {
} }
return return
} }
await new Promise((resolve) => setTimeout(resolve, checkInterval)) await new Promise((resolve) => setTimeout(resolve, NIP07_INJECT_CHECK_INTERVAL_MS))
} }
throw new Error( throw new Error(

Loading…
Cancel
Save