diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index c909025a..f68271e8 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -18,6 +18,7 @@ dependencies { implementation project(':capacitor-share') implementation project(':capacitor-status-bar') implementation project(':capacitor-toast') + implementation project(':capacitor-secure-storage-plugin') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 8a2fdb13..e770f7af 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -28,3 +28,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@c include ':capacitor-toast' project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast/android') + +include ':capacitor-secure-storage-plugin' +project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index b866717b..99f2ff5c 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -20,6 +20,7 @@ def capacitor_pods pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share' pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/status-bar' pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast' + pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin' end target 'App' do diff --git a/package.json b/package.json index b5c1c96c..f83a724f 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@solid-primitives/upload": "^0.0.111", "@solidjs/meta": "^0.29.1", "@solidjs/router": "^0.9.0", + "capacitor-secure-storage-plugin": "^0.9.0", "i18next": "^22.5.1", "i18next-browser-languagedetector": "^7.1.0", "qr-scanner": "^1.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18ba86b4..59775269 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@solidjs/router': specifier: ^0.9.0 version: 0.9.0(solid-js@1.8.5) + capacitor-secure-storage-plugin: + specifier: ^0.9.0 + version: 0.9.0(@capacitor/core@5.5.1) i18next: specifier: ^22.5.1 version: 22.5.1 @@ -6320,6 +6323,14 @@ packages: /caniuse-lite@1.0.30001559: resolution: {integrity: sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA==} + /capacitor-secure-storage-plugin@0.9.0(@capacitor/core@5.5.1): + resolution: {integrity: sha512-P5fiC94opcLHu41vceo9weXH+20g0SPYKkeAx+qm9eKNcVFqpcuI4dqwivXlGXYNMDygyjSQuAaFwZ4gW0Y91Q==} + peerDependencies: + '@capacitor/core': ^5.0.0 + dependencies: + '@capacitor/core': 5.5.1 + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} diff --git a/src/components/PendingNwc.tsx b/src/components/PendingNwc.tsx index 7bacd417..ae4111e9 100644 --- a/src/components/PendingNwc.tsx +++ b/src/components/PendingNwc.tsx @@ -1,3 +1,4 @@ +import { TagItem } from "@mutinywallet/mutiny-wasm"; import { createEffect, createResource, @@ -44,6 +45,10 @@ export function PendingNwc() { const profiles = await state.mutiny_wallet?.get_nwc_profiles(); if (!profiles) return []; + const contacts: TagItem[] | undefined = + await state.mutiny_wallet?.get_contacts_sorted(); + if (!contacts) return []; + const pending = await state.mutiny_wallet?.get_pending_nwc_invoices(); if (!pending) return []; @@ -59,6 +64,16 @@ export function PendingNwc() { date: p.expiry, amount_sats: p.amount_sats }); + } else { + const contact = contacts.find((c) => c.npub === p.npub); + if (contact) { + pendingItems.push({ + id: p.id, + name_of_connection: contact.name, + date: p.expiry, + amount_sats: p.amount_sats + }); + } } } return pendingItems; diff --git a/src/components/SyncContactsForm.tsx b/src/components/SyncContactsForm.tsx index 2dc13f9b..dfa506d4 100644 --- a/src/components/SyncContactsForm.tsx +++ b/src/components/SyncContactsForm.tsx @@ -12,8 +12,6 @@ type NostrContactsForm = { npub: string; }; -const PRIMAL_API = import.meta.env.VITE_PRIMAL; - export function SyncContactsForm() { const i18n = useI18n(); const [state, actions] = useMegaStore(); @@ -30,8 +28,7 @@ export function SyncContactsForm() { ) => { try { const npub = f.npub.trim(); - if (!PRIMAL_API) throw new Error("PRIMAL_API not set"); - await state.mutiny_wallet?.sync_nostr_contacts(PRIMAL_API, npub); + await state.mutiny_wallet?.sync_nostr_contacts(npub); actions.saveNpub(npub); } catch (e) { console.error(e); diff --git a/src/logic/mutinyWalletSetup.ts b/src/logic/mutinyWalletSetup.ts index 7ba1ba8c..a4a9a0f8 100644 --- a/src/logic/mutinyWalletSetup.ts +++ b/src/logic/mutinyWalletSetup.ts @@ -1,6 +1,8 @@ /* @refresh reload */ +import { Capacitor } from "@capacitor/core"; import initMutinyWallet, { MutinyWallet } from "@mutinywallet/mutiny-wasm"; +import { SecureStoragePlugin } from "capacitor-secure-storage-plugin"; export type Network = "bitcoin" | "testnet" | "regtest" | "signet"; @@ -16,6 +18,7 @@ export type MutinyWalletSettingStrings = { subscriptions?: string; storage?: string; scorer?: string; + primal?: string; selfhosted?: string; }; @@ -75,6 +78,11 @@ const SETTINGS_KEYS = [ storageKey: "USER_SETTINGS_scorer", default: import.meta.env.VITE_SCORER }, + { + name: "primal", + storageKey: "USER_SETTINGS_primal", + default: import.meta.env.VITE_PRIMAL + }, { name: "selfhosted", storageKey: "USER_SETTINGS_selfhosted", @@ -246,9 +254,33 @@ export async function setupMutinyWallet( auth, subscriptions, storage, - scorer + scorer, + primal } = settings; + let nsec; + // get nsec from secure storage + if (Capacitor.isNativePlatform()) { + try { + const value = await SecureStoragePlugin.get({ key: "nsec" }); + nsec = value.value; + } catch (e) { + console.log("No nsec stored"); + } + } + + // if we didn't get an nsec from storage, try to use extension + let extension_key; + if (!nsec) { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ignore nostr not existing, only does if they have extension + extension_key = await window.nostr.getPublicKey(); + } catch (_) { + console.log("No NIP-07 extension"); + } + } + console.log("Initializing Mutiny Manager"); console.log("Using network", network); console.log("Using proxy", proxy); @@ -261,6 +293,7 @@ export async function setupMutinyWallet( console.log("Using subscriptions address", subscriptions); console.log("Using storage address", storage); console.log("Using scorer address", scorer); + console.log("Using primal address", primal); console.log(safeMode ? "Safe mode enabled" : "Safe mode disabled"); console.log(shouldZapHodl ? "Hodl zaps enabled" : "Hodl zaps disabled"); @@ -290,7 +323,10 @@ export async function setupMutinyWallet( // Safe mode safeMode || undefined, // Skip hodl invoices? (defaults to true, so if shouldZapHodl is true that's when we pass false) - shouldZapHodl ? false : undefined + shouldZapHodl ? false : undefined, + nsec, + extension_key ? extension_key : undefined, + primal ); sessionStorage.setItem("MUTINY_WALLET_INITIALIZED", Date.now().toString()); diff --git a/src/routes/settings/SyncNostrContacts.tsx b/src/routes/settings/SyncNostrContacts.tsx index c5847f02..daf71cd8 100644 --- a/src/routes/settings/SyncNostrContacts.tsx +++ b/src/routes/settings/SyncNostrContacts.tsx @@ -1,4 +1,7 @@ +import { Capacitor } from "@capacitor/core"; import { createForm, required, SubmitHandler } from "@modular-forms/solid"; +import { MutinyWallet } from "@mutinywallet/mutiny-wasm"; +import { SecureStoragePlugin } from "capacitor-secure-storage-plugin"; import { createSignal, Match, Show, Switch } from "solid-js"; import { @@ -24,13 +27,13 @@ type NostrContactsForm = { npub: string; }; -const PRIMAL_API = import.meta.env.VITE_PRIMAL; - function SyncContactsForm() { const i18n = useI18n(); const [state, actions] = useMegaStore(); const [error, setError] = createSignal(); + const allowNsec = Capacitor.isNativePlatform(); + const [feedbackForm, { Form, Field }] = createForm({ initialValues: { npub: "" @@ -41,9 +44,27 @@ function SyncContactsForm() { f: NostrContactsForm ) => { try { - const npub = f.npub.trim(); - if (!PRIMAL_API) throw new Error("PRIMAL_API not set"); - await state.mutiny_wallet?.sync_nostr_contacts(PRIMAL_API, npub); + const string = f.npub.trim(); + let npub = string; + + // if it is an nsec, save it into secure storage + if (string.startsWith("nsec")) { + if (!allowNsec) { + throw new Error( + "nsec not allowed in web version, please install the app" + ); + } + + // set in storage + SecureStoragePlugin.set({ key: "nsec", value: string }).then( + (success) => console.log(success) + ); + + // set npub and continue + npub = await MutinyWallet.nsec_to_npub(string); + } + + await state.mutiny_wallet?.sync_nostr_contacts(npub); actions.saveNpub(npub); } catch (e) { console.error(e); @@ -68,7 +89,7 @@ function SyncContactsForm() { value={field.value} error={field.error} label={i18n.t("settings.nostr_contacts.npub_label")} - placeholder="npub..." + placeholder={allowNsec ? "npub/nsec..." : "npub..."} /> )} @@ -97,17 +118,18 @@ export function SyncNostrContacts() { const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(); - function clearNpub() { + async function clearNpub() { actions.saveNpub(""); + if (Capacitor.isNativePlatform()) { + await SecureStoragePlugin.remove({ key: "nsec" }); + } } async function resync() { setError(undefined); setLoading(true); try { - if (!PRIMAL_API) throw new Error("PRIMAL_API not set"); await state.mutiny_wallet?.sync_nostr_contacts( - PRIMAL_API, // We can only see the resync button if there's an npub set state.npub! ); diff --git a/src/utils/fetchZaps.ts b/src/utils/fetchZaps.ts index f2a85894..a36154aa 100644 --- a/src/utils/fetchZaps.ts +++ b/src/utils/fetchZaps.ts @@ -116,9 +116,13 @@ async function simpleZapFromEvent( } } +// todo remove const PRIMAL_API = import.meta.env.VITE_PRIMAL; -async function fetchFollows(npub: string): Promise { +async function fetchFollows( + primal_url: string, + npub: string +): Promise { let pubkey = undefined; try { pubkey = await hexpubFromNpub(npub); @@ -127,7 +131,7 @@ async function fetchFollows(npub: string): Promise { throw err; } - const response = await fetch(PRIMAL_API, { + const response = await fetch(primal_url, { method: "POST", headers: { "Content-Type": "application/json" @@ -167,9 +171,10 @@ type PrimalResponse = NostrEvent | NostrProfile; async function fetchZapsFromPrimal( follows: string[], + primal_url?: string, until?: number ): Promise { - if (!PRIMAL_API) throw new Error("Missing PRIMAL_API environment variable"); + if (!primal_url) throw new Error("Missing PRIMAL_API environment variable"); const query = { kinds: [9735, 0, 10000113], @@ -183,7 +188,7 @@ async function fetchZapsFromPrimal( until ? { ...query, since: until } : query ]); - const response = await fetch(PRIMAL_API, { + const response = await fetch(primal_url, { method: "POST", headers: { "Content-Type": "application/json" @@ -224,16 +229,21 @@ export const fetchZaps: ResourceFetcher< info.value?.profiles || {}; let newUntil = undefined; - if (!PRIMAL_API) + const primal_url = state.settings?.primal; + if (!primal_url) throw new Error("Missing PRIMAL_API environment variable"); // Only have to ask the relays for follows one time if (follows.length === 0) { - follows = await fetchFollows(npub); + follows = await fetchFollows(primal_url, npub); } // Ask primal for all the zaps for these follow pubkeys - const data = await fetchZapsFromPrimal(follows, info?.value?.until); + const data = await fetchZapsFromPrimal( + follows, + primal_url, + info?.value?.until + ); // Parse the primal response for (const object of data) {