All files / src mode.ts

94.44% Statements 51/54
82.85% Branches 29/35
100% Functions 11/11
94.44% Lines 51/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139450x             1x 113x     1x 32x 20x   12x 4x   8x 1x   7x     1x 66x 3x   63x                     1x 55x 55x 29x   26x 1x   25x 25x 15x 3x   12x   22x 10x   22x     1x 27x     27x 27x     1x 24x     24x     24x 24x   24x 24x 24x           7x       24x           5x     24x       1x         19x       19x                   2x           5x       5x            
import {Cancelable, LoginContext, PromptResponse} from '@wharfkit/session'
 
import {Translator} from './transports/types'
 
/** Anchor transport used for login and signing: browser authenticator or native app. */
export type AnchorMode = 'web' | 'app'
 
function isValidMode(value: unknown): value is AnchorMode {
    return value === 'web' || value === 'app'
}
 
export function readMode(data: Record<string, any>): AnchorMode | undefined {
    if (isValidMode(data.mode)) {
        return data.mode
    }
    if (data.channelUrl || data.signerKey) {
        return 'app'
    }
    if (data.encryptionKey && data.messageKey) {
        return 'web'
    }
    return undefined
}
 
export function writeMode(data: Record<string, any>, mode: AnchorMode) {
    if (!isValidMode(mode)) {
        throw new Error(`Invalid Anchor mode: ${mode}`)
    }
    data.mode = mode
}
 
/** Per-call options from `login({arbitrary: {anchor: {...}}})`. */
export interface AnchorLoginOptions {
    mode?: AnchorMode
    /** Window the caller opened inside its own click, for the web transport to navigate. */
    popup?: Window
}
 
/** Read one plugin's entry out of the shared arbitrary bag. Throws on a bad value. */
export function readLoginOptions(id: string, arbitrary?: Record<string, any>): AnchorLoginOptions {
    const options = arbitrary?.[id]
    if (options === undefined || options === null) {
        return {}
    }
    if (typeof options !== 'object') {
        throw new Error(`Invalid Anchor login options: ${options}`)
    }
    const result: AnchorLoginOptions = {}
    if (options.mode !== undefined) {
        if (!isValidMode(options.mode)) {
            throw new Error(`Invalid Anchor mode: ${options.mode}`)
        }
        result.mode = options.mode
    }
    if (options.popup) {
        result.popup = options.popup
    }
    return result
}
 
export function ledgerTransportAvailable(): boolean {
    Iif (typeof navigator === 'undefined') {
        return false
    }
    const nav = navigator as any
    return Boolean(nav.hid) || Boolean(nav.usb)
}
 
function modeElements(t: Translator, onChoice: (mode: AnchorMode) => void, includeWeb: boolean) {
    const webLabel = t('mode.web.label', {
        default: 'In this browser using anchorwallet.io',
    })
    const webDetail = ledgerTransportAvailable()
        ? t('mode.web.detail', {default: 'with a passkey or Ledger'})
        : t('mode.web.detail_no_ledger', {default: 'with a passkey'})
    const appLabel = t('mode.app.label', {default: 'With the Anchor app'})
    const appDetail = t('mode.app.detail', {default: 'on this or another device'})
 
    const elements: any[] = []
    if (includeWeb) {
        elements.push({
            type: 'button',
            label: webLabel,
            data: {
                label: `${webLabel}\n${webDetail}`,
                variant: 'primary',
                onClick: () => onChoice('web'),
            },
        })
    }
    elements.push({
        type: 'button',
        label: appLabel,
        data: {
            label: `${appLabel}\n${appDetail}`,
            variant: includeWeb ? 'secondary' : 'primary',
            onClick: () => onChoice('app'),
        },
    })
    return elements
}
 
/** Two buttons, no QR, no sub-product logos. `onChoice` fires synchronously inside the click. */
export function promptForMode(
    context: LoginContext,
    t: Translator,
    onChoice: (mode: AnchorMode) => void
): Cancelable<PromptResponse> {
    Iif (!context.ui) {
        throw new Error('No UI available')
    }
 
    return context.ui.prompt({
        title: t('mode.title', {default: 'How do you use Anchor?'}),
        body: t('mode.body', {
            default:
                'Choose where to approve requests. You can choose differently the next time you log in.',
        }),
        elements: modeElements(t, onChoice, true),
    })
}
 
export function promptForRecovery(
    context: LoginContext,
    t: Translator,
    includeWeb: boolean,
    onChoice: (mode: AnchorMode) => void
): Cancelable<PromptResponse> {
    Iif (!context.ui) {
        throw new Error('No UI available')
    }
 
    return context.ui.prompt({
        title: t('recovery.title', {default: 'Request cancelled'}),
        body: t('recovery.body', {default: 'Choose how you want to try again.'}),
        elements: modeElements(t, onChoice, includeWeb),
    })
}