From 54513994901c16259c7666057b38029dc3769857 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Sun, 26 Nov 2023 14:47:17 +0100 Subject: [PATCH 01/44] add filter for invalid relay urls in publish --- src/nostr/class/Pool.ts | 5 +++-- src/nostr/util.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/nostr/class/Pool.ts b/src/nostr/class/Pool.ts index 0121d4ba..e35f0086 100644 --- a/src/nostr/class/Pool.ts +++ b/src/nostr/class/Pool.ts @@ -10,7 +10,7 @@ import { finishEvent, SimplePool, validateEvent } from 'nostr-tools' import { defaultRelays, enutsPubkey, EventKind } from '../consts' import { encrypt } from '../crypto' -import { normalizeURL, parseUserRelays } from '../util' +import { hasRelayValidPrefix, normalizeURL, parseUserRelays } from '../util' interface IPoolSubArgs { filter: IPoolSubProps, @@ -186,7 +186,8 @@ class Pool { const userRelays = cTo(storedRelays || '[]') const relaysToPublish = [...userRelays, ...defaultRelays] const recipientRelays = await this.#getRelaysByHex(nostr.contact.hex, relaysToPublish) - const published = this.#publishEventToPool(event, sk, [...recipientRelays, ...relaysToPublish]) + const allRelays = [...recipientRelays, ...relaysToPublish].filter(hasRelayValidPrefix) + const published = this.#publishEventToPool(event, sk, allRelays) if (!published) { return false } // save recipient hex to get the conversation later on await updateNostrDmUsers(nostr.contact) diff --git a/src/nostr/util.ts b/src/nostr/util.ts index e635f681..6a8dd0ef 100644 --- a/src/nostr/util.ts +++ b/src/nostr/util.ts @@ -158,4 +158,9 @@ export function shuffle(a: T[]) { [a[i], a[j]] = [a[j], a[i]] } return a +} + +export function hasRelayValidPrefix(str: string) { + const validPrefixes = ['http', 'https', 'ws', 'wss'] + return validPrefixes.some(prefix => str.startsWith(prefix)) } \ No newline at end of file From 276ab992ea1cda64402a443366e1b5f5cfa08262 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Sun, 26 Nov 2023 14:51:28 +0100 Subject: [PATCH 02/44] bump to 0.1.3-beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5eca590..0f985ff1 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,7 @@ "blind-signatures", "lightning-network" ], - "version": "0.1.2", + "version": "0.1.3", "license": "AGPL-3.0-only", "bugs": { "url": "https://github.com/cashubtc/eNuts/issues" From 3490be0f4a54ed18c88e87be54eb5b71e0cd878c Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Sun, 26 Nov 2023 17:29:52 +0100 Subject: [PATCH 03/44] add methods to check for new release --- src/consts/urls.ts | 3 ++- src/context/Prompt.tsx | 10 ++++++- src/model/github.ts | 57 +++++++++++++++++++++++++++++++++++++++ src/model/index.ts | 1 + src/screens/Dashboard.tsx | 12 ++++++++- src/util/github.ts | 14 ++++++++++ 6 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 src/model/github.ts create mode 100644 src/util/github.ts diff --git a/src/consts/urls.ts b/src/consts/urls.ts index 167f5d7b..7d4b29d4 100644 --- a/src/consts/urls.ts +++ b/src/consts/urls.ts @@ -1 +1,2 @@ -export const repoIssueUrl = 'https://github.com/cashubtc/eNuts/issues/new' \ No newline at end of file +export const repoIssueUrl = 'https://github.com/cashubtc/eNuts/issues/new' +export const latestReleaseUrl = 'https://api.github.com/repos/cashubtc/eNuts/releases/latest' \ No newline at end of file diff --git a/src/context/Prompt.tsx b/src/context/Prompt.tsx index 9145cf44..76e34a07 100644 --- a/src/context/Prompt.tsx +++ b/src/context/Prompt.tsx @@ -1,8 +1,13 @@ import { l } from '@log' import type { IOpenPromptAutoCloseProps, IPromptState } from '@model' +import { RootStackParamList } from '@model/nav' +import { type NavigationProp, useNavigation } from '@react-navigation/core' import { createContext, useContext, useRef, useState } from 'react' +type StackNavigation = NavigationProp + const usePrompt = () => { + const nav = useNavigation() const timerId = useRef>() const [prompt, setPrompt] = useState({ open: false, msg: '' }) @@ -18,11 +23,14 @@ const usePrompt = () => { timerId.current = undefined } - const openPrompt = (msg: string, success?: boolean) => setPrompt({ open: true, success, msg }) + const openPrompt = (msg: string, success?: boolean, showVersion?: boolean) => setPrompt({ open: true, success, msg, showVersion }) const closePrompt = () => { setPrompt({ open: false, msg: '' }) if (timerId.current) { clearTimer() } + if (prompt.showVersion) { + nav.navigate('Settings') + } } const openPromptAutoClose = ({ msg, success, ms }: IOpenPromptAutoCloseProps) => { diff --git a/src/model/github.ts b/src/model/github.ts new file mode 100644 index 00000000..8cd0d42b --- /dev/null +++ b/src/model/github.ts @@ -0,0 +1,57 @@ +export interface GithubLatest { + url: string + assets_url: string + upload_url: string + html_url: string + id: number + author: Author + node_id: string + tag_name: string + target_commitish: string + name: string + draft: boolean + prerelease: boolean + created_at: Date + published_at: Date + assets: Asset[] + tarball_url: string + zipball_url: string + body: string +} + +export interface Asset { + url: string + id: number + node_id: string + name: string + label: null + uploader: Author + content_type: string + state: string + size: number + download_count: number + created_at: Date + updated_at: Date + browser_download_url: string +} + +export interface Author { + login: string + id: number + node_id: string + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: string + site_admin: boolean +} \ No newline at end of file diff --git a/src/model/index.ts b/src/model/index.ts index 05a94de1..a011e25b 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -147,6 +147,7 @@ export interface IPromptState { open: boolean success?: boolean msg: string + showVersion?: boolean } export interface IOpenPromptAutoCloseProps { diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index f4eedb63..8e423966 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -27,6 +27,7 @@ import { addToHistory } from '@store/latestHistoryEntries' import { saveDefaultOnInit } from '@store/mintStore' import { highlight as hi, mainColors } from '@styles' import { extractStrFromURL, getStrFromClipboard, hasTrustedMint, isCashuToken, isErr, isLnInvoice, isStr } from '@util' +import { extractVersion, getLatestVersion } from '@util/github' import { claimToken, getMintsForPayment } from '@wallet' import { getTokenInfo } from '@wallet/proofs' import { useEffect, useState } from 'react' @@ -34,6 +35,8 @@ import { useTranslation } from 'react-i18next' import { TouchableOpacity, View } from 'react-native' import { s, ScaledSheet, vs } from 'react-native-size-matters' +import { version } from '../../package.json' + export default function Dashboard({ navigation, route }: TDashboardPageProps) { const { t } = useTranslation([NS.common]) // The URL content that redirects to this app after clicking on it (cashu:) @@ -46,7 +49,7 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { const { nutPub } = useNostrContext().nostr const { loading, startLoading, stopLoading } = useLoading() // Prompt modal - const { openPromptAutoClose } = usePromptContext() + const { openPromptAutoClose, openPrompt } = usePromptContext() // Cashu token hook const { token, @@ -252,6 +255,13 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { })) clearTimeout(t) }, 1000) + // check for new app version + if (userHasMints) { + const { tag_name } = await getLatestVersion() + const latest = extractVersion(tag_name) + if (latest === version) { return } + openPrompt('New version available', true, true) + } })() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/src/util/github.ts b/src/util/github.ts new file mode 100644 index 00000000..a3bbc000 --- /dev/null +++ b/src/util/github.ts @@ -0,0 +1,14 @@ +import { latestReleaseUrl } from '@consts/urls' +import type { GithubLatest } from '@model/github' + +export function getLatestVersion() { + return fetch(latestReleaseUrl) + .then(res => res.json()) + .then(json => json) as Promise +} + +export function extractVersion(version: string) { + const startIndex = version.indexOf('v') + 1 + const endIndex = version.indexOf('-', startIndex) + return version.slice(startIndex, endIndex) +} \ No newline at end of file From b5da3231732fed50a0e35c94337e7ac4dbd1d407 Mon Sep 17 00:00:00 2001 From: First-Terraner Date: Sun, 26 Nov 2023 20:46:14 +0100 Subject: [PATCH 04/44] add release screen --- src/components/Icons.tsx | 16 ++++ src/components/nav/Navigator.tsx | 2 + src/consts/urls.ts | 5 +- src/context/Prompt.tsx | 10 ++- src/model/index.ts | 3 + src/model/nav.ts | 7 +- src/screens/Dashboard.tsx | 6 +- src/screens/Payment/Success.tsx | 1 + src/screens/Release.tsx | 143 +++++++++++++++++++++++++++++++ 9 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/screens/Release.tsx diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index aa9fa3b7..5fe872da 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -553,6 +553,22 @@ export function ImageIcon({ width, height, color }: TIconProps) { ) } +export function DownloadIcon({ width, height, color }: TIconProps) { + return ( + + + + + + ) +} +export function AppleIcon({ width, height, color }: TIconProps) { + return ( + + + + ) +} const styles = StyleSheet.create({ nostrIcon: { marginLeft: -5 diff --git a/src/components/nav/Navigator.tsx b/src/components/nav/Navigator.tsx index 0f6e0de6..483b7870 100644 --- a/src/components/nav/Navigator.tsx +++ b/src/components/nav/Navigator.tsx @@ -34,6 +34,7 @@ import MintConfirmScreen from '@screens/QRScan/MintConfirm' import NpubConfirmScreen from '@screens/QRScan/NpubConfirm' import QRProcessingScreen from '@screens/QRScan/QRProcessing' import ScanSuccessScreen from '@screens/QRScan/ScanSuccess' +import ReleaseScreen from '@screens/Release' import Settings from '@screens/Settings' import AboutSettings from '@screens/Settings/About' import BackupPage from '@screens/Settings/Backup' @@ -169,6 +170,7 @@ export default function Navigator({ + diff --git a/src/consts/urls.ts b/src/consts/urls.ts index 7d4b29d4..ab7eb857 100644 --- a/src/consts/urls.ts +++ b/src/consts/urls.ts @@ -1,2 +1,5 @@ export const repoIssueUrl = 'https://github.com/cashubtc/eNuts/issues/new' -export const latestReleaseUrl = 'https://api.github.com/repos/cashubtc/eNuts/releases/latest' \ No newline at end of file +export const latestReleaseUrl = 'https://api.github.com/repos/cashubtc/eNuts/releases/latest' +export const latestApkUrl = (v: string) => `https://www.enuts.cash/apk/enuts-${v}.apk` +export const releaseUrl = (v: string) => `https://github.com/cashubtc/eNuts/releases/tag/${v}` +export const testflightUrl = 'https://testflight.apple.com/join/2pXy9lZ1' \ No newline at end of file diff --git a/src/context/Prompt.tsx b/src/context/Prompt.tsx index 76e34a07..59837ad8 100644 --- a/src/context/Prompt.tsx +++ b/src/context/Prompt.tsx @@ -2,6 +2,7 @@ import { l } from '@log' import type { IOpenPromptAutoCloseProps, IPromptState } from '@model' import { RootStackParamList } from '@model/nav' import { type NavigationProp, useNavigation } from '@react-navigation/core' +import type { GithubLatest } from '@src/model/github' import { createContext, useContext, useRef, useState } from 'react' type StackNavigation = NavigationProp @@ -23,13 +24,18 @@ const usePrompt = () => { timerId.current = undefined } - const openPrompt = (msg: string, success?: boolean, showVersion?: boolean) => setPrompt({ open: true, success, msg, showVersion }) + const openPrompt = ( + msg: string, + success?: boolean, + showVersion?: boolean, + releaseInfo?: GithubLatest + ) => setPrompt({ open: true, success, msg, showVersion, releaseInfo }) const closePrompt = () => { setPrompt({ open: false, msg: '' }) if (timerId.current) { clearTimer() } if (prompt.showVersion) { - nav.navigate('Settings') + nav.navigate('Release', { info: prompt.releaseInfo }) } } diff --git a/src/model/index.ts b/src/model/index.ts index a011e25b..8a7fb786 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -3,6 +3,8 @@ import type { HighlightKey } from '@styles' import type { ExpoConfig } from 'expo/config' import type { SQLStmtCb, SQLStmtErrCb, WebSQLDatabase } from 'expo-sqlite' +import type { GithubLatest } from './github' + export interface IExpoConfig extends ExpoConfig { extra?: { DEBUG?: string // | 'full' @@ -148,6 +150,7 @@ export interface IPromptState { success?: boolean msg: string showVersion?: boolean + releaseInfo?: GithubLatest } export interface IOpenPromptAutoCloseProps { diff --git a/src/model/nav.ts b/src/model/nav.ts index 10d3b158..1e7badc8 100644 --- a/src/model/nav.ts +++ b/src/model/nav.ts @@ -2,7 +2,8 @@ import type { EventArg } from '@react-navigation/core' import type { NativeStackScreenProps } from '@react-navigation/native-stack' import type { IHistoryEntry, IMintUrl, IMintWithBalance, IProofSelection, ITokenInfo } from '.' -import { HexKey, IContact } from './nostr' +import type { GithubLatest } from './github' +import type { HexKey, IContact } from './nostr' export interface INostrSendData { senderName: string @@ -23,6 +24,9 @@ export type RootStackParamList = { newMint?: boolean } | undefined Settings: undefined + Release: { + info?: GithubLatest + } 'General settings': undefined 'Security settings': undefined 'Privacy settings': undefined @@ -239,6 +243,7 @@ export type TQRScanPageProps = NativeStackScreenProps export type THistoryEntryPageProps = NativeStackScreenProps export type TSettingsPageProps = NativeStackScreenProps +export type TReleasePageProps = NativeStackScreenProps export type TGeneralSettingsPageProps = NativeStackScreenProps export type TDisplaySettingsPageProps = NativeStackScreenProps export type TSecuritySettingsPageProps = NativeStackScreenProps diff --git a/src/screens/Dashboard.tsx b/src/screens/Dashboard.tsx index 8e423966..bed64455 100644 --- a/src/screens/Dashboard.tsx +++ b/src/screens/Dashboard.tsx @@ -257,10 +257,10 @@ export default function Dashboard({ navigation, route }: TDashboardPageProps) { }, 1000) // check for new app version if (userHasMints) { - const { tag_name } = await getLatestVersion() - const latest = extractVersion(tag_name) + const releaseInfo = await getLatestVersion() + const latest = extractVersion(releaseInfo.tag_name) if (latest === version) { return } - openPrompt('New version available', true, true) + openPrompt('New version available', true, true, releaseInfo) } })() // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/screens/Payment/Success.tsx b/src/screens/Payment/Success.tsx index 5b0a48f0..5ace0b4b 100644 --- a/src/screens/Payment/Success.tsx +++ b/src/screens/Payment/Success.tsx @@ -41,6 +41,7 @@ export default function SuccessPage({ navigation, route }: TSuccessPageProps) { size={s(100)} hex={nostr.contact.hex} uri={nostr.contact.picture} + isUser /> : diff --git a/src/screens/Release.tsx b/src/screens/Release.tsx new file mode 100644 index 00000000..a45dc2fe --- /dev/null +++ b/src/screens/Release.tsx @@ -0,0 +1,143 @@ +import Blank from '@comps/Blank' +import { AppleIcon, DownloadIcon, EyeClosedIcon, EyeIcon, GithubIcon } from '@comps/Icons' +import Screen from '@comps/Screen' +import Txt from '@comps/Txt' +import { isIOS } from '@consts' +import { latestApkUrl, releaseUrl, testflightUrl } from '@consts/urls' +import type { TReleasePageProps } from '@model/nav' +import { usePromptContext } from '@src/context/Prompt' +import { useThemeContext } from '@src/context/Theme' +import { NS } from '@src/i18n' +import { mainColors } from '@styles' +import { H_Colors } from '@styles/colors' +import { getShortDateStr, isErr, openUrl } from '@util' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView, TouchableOpacity, View } from 'react-native' +import { s, ScaledSheet } from 'react-native-size-matters' + +import ProfilePic from './Addressbook/ProfilePic' + +export default function ReleaseScreen({ navigation, route }: TReleasePageProps) { + const { t } = useTranslation([NS.common]) + const { info } = route.params + const { color } = useThemeContext() + const { openPromptAutoClose } = usePromptContext() + const [showBody, setShowBody] = useState(false) + if (!info) { return } + return ( + navigation.goBack()} + > + + + + + + + + + + : } + onPress={() => { + void openUrl(isIOS ? testflightUrl : latestApkUrl(info.tag_name))?.catch(e => + openPromptAutoClose({ msg: isErr(e) ? e.message : t('deepLinkErr') })) + }} + /> + } + onPress={() => { + void openUrl(releaseUrl(info.tag_name))?.catch(e => + openPromptAutoClose({ msg: isErr(e) ? e.message : t('deepLinkErr') })) + }} + /> + : } + onPress={() => setShowBody(prev => !prev)} + /> + + {showBody && + + + + } + + ) +} + +function Badge({ txt }: { txt: string }) { + return ( + + + + ) +} + +function ActionBtn({ txt, onPress, icon }: { txt: string, onPress: () => void, icon?: React.ReactNode }) { + const { color } = useThemeContext() + return ( + + + {icon} + + ) +} + +const styles = ScaledSheet.create({ + container: { + paddingHorizontal: '20@s', + }, + badge: { + alignSelf: 'flex-start', + backgroundColor: H_Colors.Default, + paddingHorizontal: '6@s', + paddingVertical: '3@s', + borderRadius: '5@s', + }, + author: { + flexDirection: 'row', + alignItems: 'center', + marginTop: '10@vs', + paddingBottom: '10@vs', + }, + pressable: { + paddingVertical: '15@vs', + paddingHorizontal: '20@s', + borderRadius: '5@s', + marginVertical: '5@vs', + flexDirection: 'row', + justifyContent: 'space-between', + }, + notesBody: { + marginTop: '10@vs', + paddingTop: '10@vs', + }, + body: { + marginBottom: '20@vs', + paddingBottom: '20@vs', + } +}) \ No newline at end of file From a5f9de24bdf35e284830314897d6994306fb265d Mon Sep 17 00:00:00 2001 From: KKA11010 Date: Tue, 28 Nov 2023 14:07:19 +0100 Subject: [PATCH 05/44] fix pasting invoice --- src/screens/Payment/Send/Inputfield.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/screens/Payment/Send/Inputfield.tsx b/src/screens/Payment/Send/Inputfield.tsx index 6d56ecd6..60d951e2 100644 --- a/src/screens/Payment/Send/Inputfield.tsx +++ b/src/screens/Payment/Send/Inputfield.tsx @@ -10,7 +10,7 @@ import { usePromptContext } from '@src/context/Prompt' import { useThemeContext } from '@src/context/Theme' import { NS } from '@src/i18n' import { globals } from '@styles' -import { decodeLnInvoice, getStrFromClipboard, isErr, isLnurl, openUrl } from '@util' +import { decodeLnInvoice, getStrFromClipboard, isErr, isLnInvoice, isLnurl, openUrl } from '@util' import { checkFees } from '@wallet' import { createRef, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -86,6 +86,7 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP const { timeLeft } = decodeLnInvoice(input) // Invoice expired if (timeLeft <= 0) { + setInput('') return openPromptAutoClose({ msg: t('expired') + '!' }) } // navigate to coin selection screen @@ -118,6 +119,8 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP screenName={t('cashOut')} withBackBtn handlePress={() => navigation.goBack()} + mintBalance={balance} + disableMintBalance /> {!input.length && @@ -160,7 +163,12 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP keyboardType='email-address' placeholder={t('invoiceOrLnurl')} value={input} - onChangeText={setInput} + onChangeText={text => { + setInput(text) + if (isLnInvoice(text)) { + void handleInvoicePaste(text) + } + }} onSubmitEditing={() => void handleBtnPress()} autoFocus ms={200} @@ -178,8 +186,9 @@ export default function InputfieldScreen({ navigation, route }: TMeltInputfieldP