feat: new onboarding process
This commit is contained in:
@@ -205,6 +205,15 @@ import Order from './views/Order';
|
||||
import Intro from './views/Intro';
|
||||
import IntroSplash from './views/IntroSplash';
|
||||
|
||||
// Onboarding
|
||||
import Onboarding1 from './views/Onboarding/Onboarding1';
|
||||
import RecommendedSettings from './views/Onboarding/RecommendedSettings';
|
||||
import WalletSetup from './views/Onboarding/WalletSetup';
|
||||
import WalletType from './views/Onboarding/WalletType';
|
||||
import NodeChoice from './views/Onboarding/NodeChoice';
|
||||
import MintDiscovery from './views/Onboarding/MintDiscovery';
|
||||
import WalletSettings from './views/Onboarding/WalletSettings';
|
||||
|
||||
import EditFee from './views/EditFee';
|
||||
|
||||
// Embedded LND
|
||||
@@ -514,6 +523,48 @@ export default class App extends React.PureComponent {
|
||||
IntroSplash
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Onboarding1" // @ts-ignore:next-line
|
||||
component={
|
||||
Onboarding1
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="RecommendedSettings" // @ts-ignore:next-line
|
||||
component={
|
||||
RecommendedSettings
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="WalletSetup" // @ts-ignore:next-line
|
||||
component={
|
||||
WalletSetup
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="WalletType" // @ts-ignore:next-line
|
||||
component={
|
||||
WalletType
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NodeChoice" // @ts-ignore:next-line
|
||||
component={
|
||||
NodeChoice
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MintDiscovery" // @ts-ignore:next-line
|
||||
component={
|
||||
MintDiscovery
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="WalletSettings" // @ts-ignore:next-line
|
||||
component={
|
||||
WalletSettings
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Intro" // @ts-ignore:next-line
|
||||
component={Intro}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 522 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 548 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 384 KiB |
@@ -696,6 +696,7 @@
|
||||
"views.NetworkInfo.maxOutDegree": "Max out degree",
|
||||
"views.NodeQRScanner.error": "Scanned QR code was not a valid Lightning Node",
|
||||
"views.Intro.whatIsZeus": "What is Zeus?",
|
||||
"views.Intro.getStarted": "Get started",
|
||||
"views.Intro.quickStart": "Quick start",
|
||||
"views.Intro.quickStartExplainer": "Your own node on the phone",
|
||||
"views.Intro.lightningOnboarding": "Learn about lightning onboarding",
|
||||
@@ -713,6 +714,54 @@
|
||||
"views.Intro.carousel4.title": "Control the lightning",
|
||||
"views.Intro.carousel4.text": "Go beyond a basic bitcoin wallet and manage your lightning channels, liquidity, and so much more.",
|
||||
"views.Intro.errorCreatingWallet": "Error creating wallet. Restart the app and try again.",
|
||||
"views.WalletSetup.howToUseZeus": "How do you want to use ZEUS?",
|
||||
"views.WalletSetup.createNewWallet": "Create a new wallet",
|
||||
"views.WalletSetup.connectRemoteNode": "Connect to a remote Lightning node",
|
||||
"views.WalletType.whatsImportant": "What's more important to you?",
|
||||
"views.WalletType.control": "Control",
|
||||
"views.WalletType.lowFees": "Low fees",
|
||||
"views.WalletType.selfCustody.info1": "Self-custody means you control your own Bitcoin private keys. Your funds are secured by a seed phrase that only you know.",
|
||||
"views.WalletType.selfCustody.info2": "With self-custody, no third party can freeze, seize, or censor your funds. You have complete financial sovereignty.",
|
||||
"views.WalletType.selfCustody.info3": "This option requires purchasing a Lightning channel to send and receive payments. There's an upfront cost, but you'll have true ownership of your bitcoin.",
|
||||
"views.WalletType.cashu.info1": "Cashu is an ecash protocol that provides excellent privacy with lower upfront costs. It's a great way to get started with Lightning.",
|
||||
"views.WalletType.cashu.info2": "While technically custodial, Cashu mints cannot see your balance or transaction history. Your privacy is protected by cryptographic blinding.",
|
||||
"views.WalletType.cashu.info3": "ZEUS will prompt you to upgrade to self-custody once you have enough bitcoin to purchase a Lightning channel. It's a stepping stone to full sovereignty.",
|
||||
"views.Onboarding.setupQuestion": "How do you want to set up ZEUS?",
|
||||
"views.Onboarding.recommended": "ZEUS' recommended settings",
|
||||
"views.Onboarding.questionnaire": "Run through the questionnaire",
|
||||
"views.NodeChoice.whatMatters": "What do you value more in a wallet?",
|
||||
"views.NodeChoice.speedReliability": "Speed and reliability",
|
||||
"views.NodeChoice.privacyPower": "Privacy and power user features",
|
||||
"views.NodeChoice.ldkNode.info1": "LDK Node is a next-generation Lightning implementation that offers fast startup times, reliable payment routing, and a streamlined experience.",
|
||||
"views.NodeChoice.ldkNode.info2": "It uses Rapid Gossip Sync for near-instant graph updates and cloud backup via VSS (Versioned Storage Service) so your channels are always recoverable.",
|
||||
"views.NodeChoice.ldkNode.info3": "LDK Node is the recommended choice for most users who want a smooth, reliable Lightning experience on mobile.",
|
||||
"views.NodeChoice.embeddedLnd.info1": "Embedded LND is a battle-tested Lightning implementation with a rich set of power user features including custom channel management and advanced routing.",
|
||||
"views.NodeChoice.embeddedLnd.info2": "It connects to the Bitcoin network using Neutrino (BIP 157/158) for enhanced privacy — your node doesn't reveal which addresses or transactions belong to you.",
|
||||
"views.NodeChoice.embeddedLnd.info3": "Choose this if you prioritize privacy, want full control over your channels, or need advanced features like custom routing policies.",
|
||||
"views.MintDiscovery.title": "Mint discovery",
|
||||
"views.MintDiscovery.subtitle": "Choose how to discover Cashu mints",
|
||||
"views.MintDiscovery.topMints": "Top recommended mints",
|
||||
"views.MintDiscovery.fetchMints": "Find mints",
|
||||
"views.MintDiscovery.next": "Next",
|
||||
"views.MintDiscovery.chooseLater": "Choose later",
|
||||
"views.MintDiscovery.chooseLater.description": "Skip mint selection for now and choose mints later in settings",
|
||||
"views.WalletSettings.title": "Settings",
|
||||
"views.WalletSettings.createWallet": "Create wallet",
|
||||
"views.RecommendedSettings.title": "Recommended settings",
|
||||
"views.RecommendedSettings.walletType": "Wallet type",
|
||||
"views.RecommendedSettings.walletType.cashu": "Cashu ecash (low fees)",
|
||||
"views.RecommendedSettings.walletType.selfCustody": "Self-custody",
|
||||
"views.RecommendedSettings.walletType.subtitle": "Start with ecash, upgrade to self-custody later",
|
||||
"views.RecommendedSettings.nodeImplementation": "Node implementation",
|
||||
"views.RecommendedSettings.mintSource": "Mint recommendations source",
|
||||
"views.RecommendedSettings.clipboard": "Clipboard access",
|
||||
"views.RecommendedSettings.off": "Off",
|
||||
"views.RecommendedSettings.disabled": "Disabled",
|
||||
"views.RecommendedSettings.fiat": "Fiat currency conversion rates",
|
||||
"views.RecommendedSettings.topMints": "Top recommended mints",
|
||||
"views.RecommendedSettings.loadingMints": "Finding the best mints...",
|
||||
"views.RecommendedSettings.createWallet": "Create wallet",
|
||||
"views.Settings.Currency.enableFiat": "Enable fiat currency display",
|
||||
"views.OpenChannel.openChannel": "Open Channel",
|
||||
"views.OpenChannel.openChannels": "Open Channels",
|
||||
"views.OpenChannel.connectPeer": "Connect Peer",
|
||||
|
||||
@@ -92,6 +92,14 @@ interface MintRecommendation {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ScoredMint {
|
||||
url: string;
|
||||
count: number;
|
||||
score: number;
|
||||
avgRating: number;
|
||||
reviewCount: number;
|
||||
}
|
||||
|
||||
export interface MintReviewRating {
|
||||
score: number;
|
||||
max: number;
|
||||
@@ -210,6 +218,7 @@ export default class CashuStore {
|
||||
@observable public mintReviews: Map<string, MintReview[]> = new Map();
|
||||
@observable public reviewerProfiles: Map<string, ReviewerProfile> =
|
||||
new Map();
|
||||
@observable public mintInfoCache: Map<string, any> = new Map();
|
||||
@observable public loadingTrustedMints = false;
|
||||
@observable public loadingReviews = false;
|
||||
@observable public submittingReview = false;
|
||||
@@ -1318,6 +1327,102 @@ export default class CashuStore {
|
||||
return { recommendations, reviews: mintReviewsMap };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates a time-weighted score for each mint based on reviews and popularity.
|
||||
* Returns the top N mints sorted by score descending.
|
||||
*
|
||||
* - Reviews with ratings are weighted by recency (exponential decay, ~90 day half-life)
|
||||
* - Mints with no rated reviews get a neutral score of 0.5
|
||||
* - Popularity (recommendation count) provides a small boost
|
||||
* - Recent bad reviews will quickly tank a misbehaving mint's score
|
||||
*/
|
||||
public getTopScoredMints = (
|
||||
count: number = 5,
|
||||
mode: 'zeus' | 'all' = 'zeus'
|
||||
): ScoredMint[] => {
|
||||
const recommendations =
|
||||
mode === 'all'
|
||||
? this.mintRecommendations
|
||||
: this.trustedMintRecommendations;
|
||||
|
||||
if (!recommendations || recommendations.length === 0) return [];
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
const scored: ScoredMint[] = recommendations.map((rec) => {
|
||||
const reviews = this.mintReviews.get(rec.url) || [];
|
||||
const ratedReviews = reviews.filter((r) => r.rating);
|
||||
|
||||
let avgRating = 0.5; // neutral default for unrated mints
|
||||
|
||||
if (ratedReviews.length > 0) {
|
||||
let weightedSum = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
for (const review of ratedReviews) {
|
||||
const ageDays = (now - review.createdAt) / 86400;
|
||||
const weight = Math.exp(-ageDays / 90);
|
||||
const normalizedRating =
|
||||
review.rating!.score / review.rating!.max;
|
||||
|
||||
weightedSum += normalizedRating * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (totalWeight > 0) {
|
||||
avgRating = weightedSum / totalWeight;
|
||||
}
|
||||
}
|
||||
|
||||
const popularityBoost = Math.log2(1 + rec.count) * 0.05;
|
||||
const finalScore = avgRating + popularityBoost;
|
||||
|
||||
return {
|
||||
url: rec.url,
|
||||
count: rec.count,
|
||||
score: finalScore,
|
||||
avgRating,
|
||||
reviewCount: ratedReviews.length
|
||||
};
|
||||
});
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, count);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches mint info (name, icon_url, etc.) for a list of mint URLs.
|
||||
* Results are cached in mintInfoCache. Skips already-cached URLs.
|
||||
*/
|
||||
@action
|
||||
public fetchMintInfoBatch = async (mintUrls: string[]) => {
|
||||
const uncached = mintUrls.filter((url) => !this.mintInfoCache.has(url));
|
||||
if (uncached.length === 0) return;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
uncached.map(async (mintUrl) => {
|
||||
const info = await Promise.race([
|
||||
CashuDevKit.fetchMintInfo(mintUrl),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 10000)
|
||||
)
|
||||
]);
|
||||
return { mintUrl, info };
|
||||
})
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
this.mintInfoCache.set(
|
||||
result.value.mintUrl,
|
||||
result.value.info
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@action
|
||||
public fetchMints = async () => {
|
||||
try {
|
||||
@@ -2528,6 +2633,41 @@ export default class CashuStore {
|
||||
this.initializing = false;
|
||||
});
|
||||
|
||||
// Auto-add initial mints from onboarding
|
||||
try {
|
||||
const settings = this.settingsStore.settings;
|
||||
const initialMintUrls = settings?.ecash?.initialMintUrls;
|
||||
if (
|
||||
initialMintUrls &&
|
||||
initialMintUrls.length > 0 &&
|
||||
this.mintUrls.length === 0
|
||||
) {
|
||||
console.log(
|
||||
'Adding initial mints from onboarding:',
|
||||
initialMintUrls
|
||||
);
|
||||
for (const mintUrl of initialMintUrls) {
|
||||
try {
|
||||
await this.addMint(mintUrl);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to add initial mint ${mintUrl}:`,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
// Clear initialMintUrls from settings
|
||||
await this.settingsStore.updateSettings({
|
||||
ecash: {
|
||||
...settings.ecash,
|
||||
initialMintUrls: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error adding initial mints:', e);
|
||||
}
|
||||
|
||||
// Check status of pending items after initialization (skip when offline)
|
||||
if (!this.isOffline) {
|
||||
this.checkPendingItems();
|
||||
|
||||
@@ -164,6 +164,7 @@ interface EcashSettings {
|
||||
enableCashu: boolean;
|
||||
automaticallySweep: boolean;
|
||||
sweepThresholdSats?: number;
|
||||
initialMintUrls?: string[];
|
||||
}
|
||||
|
||||
interface SwapsSettings {
|
||||
@@ -1450,7 +1451,7 @@ export default class SettingsStore {
|
||||
privacy: {
|
||||
defaultBlockExplorer: 'mempool.space',
|
||||
customBlockExplorer: '',
|
||||
clipboard: false,
|
||||
clipboard: true,
|
||||
lurkerMode: false,
|
||||
enableMempoolRates: true,
|
||||
stealthMode: false,
|
||||
@@ -1516,7 +1517,7 @@ export default class SettingsStore {
|
||||
isBiometryEnabled: false,
|
||||
scramblePin: true,
|
||||
loginBackground: false,
|
||||
fiatEnabled: false,
|
||||
fiatEnabled: true,
|
||||
fiat: DEFAULT_FIAT,
|
||||
fiatRatesSource: DEFAULT_FIAT_RATES_SOURCE,
|
||||
// embedded node
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { optimizeNeutrinoPeers, createLndWallet } from './LndMobileUtils';
|
||||
import { createLdkNodeWallet } from './LdkNodeUtils';
|
||||
import { localeString } from './LocaleUtils';
|
||||
|
||||
import SettingsStore from '../stores/SettingsStore';
|
||||
|
||||
interface WalletCreationParams {
|
||||
settingsStore: SettingsStore;
|
||||
enableCashu: boolean;
|
||||
clipboard: boolean;
|
||||
fiatEnabled: boolean;
|
||||
selectedCurrency: string;
|
||||
fiatRatesSource: string;
|
||||
initialMintUrls?: string[];
|
||||
implementation?: string;
|
||||
onChoosingPeers: () => void;
|
||||
onCreatingWallet: () => void;
|
||||
onError: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export async function createOnboardingWallet(params: WalletCreationParams) {
|
||||
const {
|
||||
settingsStore,
|
||||
enableCashu,
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
fiatRatesSource,
|
||||
initialMintUrls,
|
||||
implementation,
|
||||
onChoosingPeers,
|
||||
onCreatingWallet,
|
||||
onError,
|
||||
onSuccess
|
||||
} = params;
|
||||
|
||||
const { setConnectingStatus, updateSettings, settings } = settingsStore;
|
||||
|
||||
const commonSettings = {
|
||||
privacy: {
|
||||
...settings.privacy,
|
||||
clipboard
|
||||
},
|
||||
fiatEnabled,
|
||||
fiat: selectedCurrency,
|
||||
fiatRatesSource,
|
||||
ecash: {
|
||||
...settings.ecash,
|
||||
enableCashu,
|
||||
...(initialMintUrls && initialMintUrls.length > 0
|
||||
? { initialMintUrls }
|
||||
: {})
|
||||
}
|
||||
};
|
||||
|
||||
if (implementation === 'ldk-node') {
|
||||
onCreatingWallet();
|
||||
|
||||
const nodeDir = uuidv4();
|
||||
|
||||
try {
|
||||
const response = await createLdkNodeWallet({
|
||||
nodeDir,
|
||||
network: 'mainnet'
|
||||
});
|
||||
|
||||
if (response && response.mnemonic) {
|
||||
const nodes = [
|
||||
{
|
||||
implementation: 'ldk-node',
|
||||
embeddedLdkNetwork: 'mainnet',
|
||||
ldkNodeDir: nodeDir,
|
||||
ldkMnemonic: response.mnemonic,
|
||||
nickname: localeString('general.defaultNodeNickname')
|
||||
}
|
||||
];
|
||||
|
||||
await updateSettings({
|
||||
nodes,
|
||||
...commonSettings
|
||||
});
|
||||
|
||||
setConnectingStatus(true);
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
} catch (e) {
|
||||
onError();
|
||||
}
|
||||
} else {
|
||||
// Default: embedded-lnd
|
||||
onChoosingPeers();
|
||||
|
||||
try {
|
||||
await optimizeNeutrinoPeers(undefined);
|
||||
} catch (e) {
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
|
||||
onCreatingWallet();
|
||||
|
||||
const lndDir = uuidv4();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await createLndWallet({ lndDir });
|
||||
} catch (e) {
|
||||
onError();
|
||||
return;
|
||||
}
|
||||
|
||||
const { wallet, seed, randomBase64 }: any = response;
|
||||
if (wallet && wallet.admin_macaroon) {
|
||||
const nodes = [
|
||||
{
|
||||
adminMacaroon: wallet.admin_macaroon,
|
||||
seedPhrase: seed.cipher_seed_mnemonic,
|
||||
walletPassword: randomBase64,
|
||||
embeddedLndNetwork: 'Mainnet',
|
||||
implementation: 'embedded-lnd',
|
||||
nickname: localeString('general.defaultNodeNickname'),
|
||||
lndDir
|
||||
}
|
||||
];
|
||||
|
||||
await updateSettings({
|
||||
nodes,
|
||||
...commonSettings
|
||||
});
|
||||
|
||||
setConnectingStatus(true);
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-154
@@ -4,28 +4,21 @@ import {
|
||||
Dimensions,
|
||||
ImageBackground,
|
||||
NativeEventSubscription,
|
||||
View,
|
||||
Text
|
||||
View
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Globe from '../assets/images/SVG/Globe.svg';
|
||||
import Wordmark from '../assets/images/SVG/wordmark-black.svg';
|
||||
|
||||
import Button from '../components/Button';
|
||||
import LoadingIndicator from '../components/LoadingIndicator';
|
||||
import Screen from '../components/Screen';
|
||||
import { ErrorMessage } from '../components/SuccessErrorMessage';
|
||||
|
||||
import SettingsStore, { LOCALE_KEYS } from '../stores/SettingsStore';
|
||||
|
||||
import {
|
||||
optimizeNeutrinoPeers,
|
||||
createLndWallet
|
||||
} from '../utils/LndMobileUtils';
|
||||
import { localeString } from '../utils/LocaleUtils';
|
||||
import { themeColor } from '../utils/ThemeUtils';
|
||||
|
||||
@@ -37,10 +30,7 @@ interface IntroSplashProps {
|
||||
}
|
||||
|
||||
interface IntroSplashState {
|
||||
choosingPeers: boolean;
|
||||
creatingWallet: boolean;
|
||||
error: boolean;
|
||||
modalBlur: number;
|
||||
}
|
||||
|
||||
@inject('SettingsStore')
|
||||
@@ -51,10 +41,7 @@ export default class IntroSplash extends React.Component<
|
||||
> {
|
||||
private backPressSubscription: NativeEventSubscription;
|
||||
state = {
|
||||
choosingPeers: false,
|
||||
creatingWallet: false,
|
||||
error: false,
|
||||
modalBlur: 90
|
||||
error: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -70,9 +57,6 @@ export default class IntroSplash extends React.Component<
|
||||
}
|
||||
|
||||
handleBackPress = () => {
|
||||
if (this.state.creatingWallet) {
|
||||
return true;
|
||||
}
|
||||
BackHandler.exitApp();
|
||||
return true;
|
||||
};
|
||||
@@ -83,7 +67,6 @@ export default class IntroSplash extends React.Component<
|
||||
|
||||
render() {
|
||||
const { navigation, SettingsStore } = this.props;
|
||||
const { creatingWallet, choosingPeers } = this.state;
|
||||
const { settings } = SettingsStore;
|
||||
const locale = settings.locale || '';
|
||||
|
||||
@@ -93,57 +76,6 @@ export default class IntroSplash extends React.Component<
|
||||
let localeName;
|
||||
if (localeItem[0]) localeName = localeItem[0].value;
|
||||
|
||||
if (choosingPeers || creatingWallet) {
|
||||
return (
|
||||
<Screen>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
top: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: Dimensions.get('window').width * 0.85,
|
||||
maxHeight: 200,
|
||||
marginTop: 10,
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
>
|
||||
<Wordmark fill={themeColor('highlight')} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
alignSelf: 'center',
|
||||
fontSize: 15,
|
||||
padding: 8
|
||||
}}
|
||||
>
|
||||
{choosingPeers
|
||||
? localeString('views.Intro.choosingPeers')
|
||||
: localeString(
|
||||
'views.Intro.creatingWallet'
|
||||
).replace('Zeus', 'ZEUS')}
|
||||
</Text>
|
||||
<View style={{ marginTop: 40 }}>
|
||||
<LoadingIndicator />
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
bottom: 56,
|
||||
position: 'absolute',
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
></View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<SafeAreaView
|
||||
@@ -243,91 +175,10 @@ export default class IntroSplash extends React.Component<
|
||||
color: themeColor('background')
|
||||
}}
|
||||
title={localeString(
|
||||
'views.Intro.quickStart'
|
||||
'views.Intro.getStarted'
|
||||
)}
|
||||
onPress={async () => {
|
||||
this.setState({
|
||||
choosingPeers: true
|
||||
});
|
||||
const { SettingsStore } = this.props;
|
||||
const {
|
||||
setConnectingStatus,
|
||||
updateSettings
|
||||
} = SettingsStore;
|
||||
|
||||
try {
|
||||
await optimizeNeutrinoPeers(
|
||||
undefined
|
||||
);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
error: true,
|
||||
choosingPeers: false,
|
||||
creatingWallet: false
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
choosingPeers: false,
|
||||
creatingWallet: true
|
||||
});
|
||||
|
||||
const lndDir = uuidv4();
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await createLndWallet({
|
||||
lndDir
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
error: true,
|
||||
choosingPeers: false,
|
||||
creatingWallet: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
wallet,
|
||||
seed,
|
||||
randomBase64
|
||||
}: any = response;
|
||||
if (wallet && wallet.admin_macaroon) {
|
||||
let nodes = [
|
||||
{
|
||||
adminMacaroon:
|
||||
wallet.admin_macaroon,
|
||||
seedPhrase:
|
||||
seed.cipher_seed_mnemonic,
|
||||
walletPassword:
|
||||
randomBase64,
|
||||
embeddedLndNetwork:
|
||||
'Mainnet',
|
||||
implementation:
|
||||
'embedded-lnd',
|
||||
nickname: localeString(
|
||||
'general.defaultNodeNickname'
|
||||
),
|
||||
lndDir,
|
||||
isSqlite: true
|
||||
}
|
||||
];
|
||||
|
||||
updateSettings({ nodes }).then(
|
||||
() => {
|
||||
setConnectingStatus(true);
|
||||
navigation.navigate(
|
||||
'Wallet'
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({
|
||||
error: true,
|
||||
creatingWallet: false
|
||||
});
|
||||
}
|
||||
onPress={() => {
|
||||
navigation.navigate('Onboarding1');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Text as RNText,
|
||||
View,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Route } from '@react-navigation/native';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import LoadingIndicator from '../../components/LoadingIndicator';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
import TextInput from '../../components/TextInput';
|
||||
|
||||
import CashuStore, { ScoredMint } from '../../stores/CashuStore';
|
||||
import SettingsStore from '../../stores/SettingsStore';
|
||||
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
import MintAvatar from '../../components/MintAvatar';
|
||||
|
||||
type DiscoverMode = 'zeus' | 'all' | 'custom' | 'later';
|
||||
|
||||
interface MintDiscoveryProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
CashuStore: CashuStore;
|
||||
SettingsStore: SettingsStore;
|
||||
route: Route<
|
||||
'MintDiscovery',
|
||||
{ enableCashu: boolean; returnTo?: string; implementation?: string }
|
||||
>;
|
||||
}
|
||||
|
||||
interface MintDiscoveryState {
|
||||
discoverMode: DiscoverMode;
|
||||
customNpub: string;
|
||||
npubError: string;
|
||||
hasFetched: boolean;
|
||||
}
|
||||
|
||||
@inject('CashuStore', 'SettingsStore')
|
||||
@observer
|
||||
export default class MintDiscovery extends React.Component<
|
||||
MintDiscoveryProps,
|
||||
MintDiscoveryState
|
||||
> {
|
||||
state: MintDiscoveryState = {
|
||||
discoverMode: 'zeus',
|
||||
customNpub: '',
|
||||
npubError: '',
|
||||
hasFetched: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Auto-fetch with default mode (ZEUS' contacts)
|
||||
this.handleFetchMints();
|
||||
}
|
||||
|
||||
validateAndSetNpub = (text: string) => {
|
||||
const { CashuStore } = this.props;
|
||||
this.setState({ customNpub: text });
|
||||
|
||||
if (text.trim() === '') {
|
||||
this.setState({ npubError: '' });
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = CashuStore.validateNpub(text);
|
||||
if (!isValid) {
|
||||
this.setState({
|
||||
npubError: localeString('views.Cashu.AddMint.invalidNpub')
|
||||
});
|
||||
} else {
|
||||
this.setState({ npubError: '' });
|
||||
}
|
||||
};
|
||||
|
||||
handleDiscoverSelect = (mode: DiscoverMode) => {
|
||||
this.setState({ discoverMode: mode }, () => {
|
||||
if (mode !== 'custom' && mode !== 'later') {
|
||||
this.handleFetchMints();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleFetchMints = () => {
|
||||
const { CashuStore } = this.props;
|
||||
const { discoverMode, customNpub, npubError } = this.state;
|
||||
|
||||
if (discoverMode === 'custom') {
|
||||
if (npubError || !customNpub.trim()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ hasFetched: true });
|
||||
|
||||
if (discoverMode === 'all') {
|
||||
CashuStore.fetchMints();
|
||||
} else if (discoverMode === 'zeus') {
|
||||
CashuStore.fetchMintsFromFollows();
|
||||
} else if (discoverMode === 'custom') {
|
||||
CashuStore.fetchMintsFromFollows(customNpub);
|
||||
}
|
||||
};
|
||||
|
||||
isFetchDisabled = () => {
|
||||
const { discoverMode, customNpub, npubError } = this.state;
|
||||
if (discoverMode === 'custom') {
|
||||
return !customNpub.trim() || !!npubError;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
handleNext = () => {
|
||||
const { navigation, route, CashuStore } = this.props;
|
||||
const { discoverMode } = this.state;
|
||||
const returnTo = route.params?.returnTo;
|
||||
const enableCashu = route.params?.enableCashu;
|
||||
const implementation = route.params?.implementation;
|
||||
|
||||
let initialMintUrls: string[] = [];
|
||||
if (discoverMode !== 'later') {
|
||||
const topMints = CashuStore.getTopScoredMints(
|
||||
5,
|
||||
discoverMode === 'all' ? 'all' : 'zeus'
|
||||
);
|
||||
initialMintUrls = topMints.map((m: ScoredMint) => m.url);
|
||||
}
|
||||
|
||||
if (returnTo) {
|
||||
navigation.navigate(returnTo, {
|
||||
discoverMode,
|
||||
initialMintUrls
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('WalletSettings', {
|
||||
enableCashu,
|
||||
initialMintUrls,
|
||||
implementation
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderRadioOption = (
|
||||
mode: DiscoverMode,
|
||||
titleKey: string,
|
||||
descriptionKey: string
|
||||
) => {
|
||||
const { discoverMode } = this.state;
|
||||
const highlightColor = themeColor('highlight');
|
||||
const isSelected = discoverMode === mode;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.optionItem,
|
||||
{ backgroundColor: themeColor('background') },
|
||||
isSelected && [
|
||||
styles.optionItemSelected,
|
||||
{ borderColor: highlightColor }
|
||||
]
|
||||
]}
|
||||
onPress={() => this.handleDiscoverSelect(mode)}
|
||||
>
|
||||
<View
|
||||
style={[styles.radioOuter, { borderColor: highlightColor }]}
|
||||
>
|
||||
{isSelected && (
|
||||
<View
|
||||
style={[
|
||||
styles.radioInner,
|
||||
{ backgroundColor: highlightColor }
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.optionTextContainer}>
|
||||
<Text
|
||||
style={{
|
||||
...styles.optionTitle,
|
||||
color: themeColor('text')
|
||||
}}
|
||||
>
|
||||
{localeString(titleKey)}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
...styles.optionDescription,
|
||||
color: themeColor('secondaryText')
|
||||
}}
|
||||
>
|
||||
{localeString(descriptionKey)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, CashuStore } = this.props;
|
||||
const { discoverMode, customNpub, npubError, hasFetched } = this.state;
|
||||
|
||||
const isLater = discoverMode === 'later';
|
||||
|
||||
const isLoading = isLater
|
||||
? false
|
||||
: discoverMode === 'all'
|
||||
? CashuStore.loading
|
||||
: CashuStore.loadingTrustedMints;
|
||||
|
||||
const topMints =
|
||||
hasFetched && !isLater
|
||||
? CashuStore.getTopScoredMints(
|
||||
5,
|
||||
discoverMode === 'all' ? 'all' : 'zeus'
|
||||
)
|
||||
: [];
|
||||
|
||||
// Fetch mint info for icons when top mints are available
|
||||
if (topMints.length > 0 && !isLoading) {
|
||||
const urls = topMints.map((m: ScoredMint) => m.url);
|
||||
const hasUncached = urls.some(
|
||||
(u: string) => !CashuStore.mintInfoCache.has(u)
|
||||
);
|
||||
if (hasUncached) {
|
||||
CashuStore.fetchMintInfoBatch(urls);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header
|
||||
leftComponent="Back"
|
||||
centerComponent={{
|
||||
text: localeString('views.MintDiscovery.title'),
|
||||
style: {
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}
|
||||
}}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flex: 1, paddingHorizontal: 15, marginTop: 5 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{localeString('views.MintDiscovery.subtitle')}
|
||||
</Text>
|
||||
|
||||
{this.renderRadioOption(
|
||||
'zeus',
|
||||
'views.Cashu.AddMint.zeusFollows',
|
||||
'views.Cashu.AddMint.zeusFollows.description'
|
||||
)}
|
||||
{this.renderRadioOption(
|
||||
'all',
|
||||
'views.Cashu.AddMint.allNostrUsers',
|
||||
'views.Cashu.AddMint.allNostrUsers.description'
|
||||
)}
|
||||
{this.renderRadioOption(
|
||||
'custom',
|
||||
'views.Cashu.AddMint.yourFollows',
|
||||
'views.Cashu.AddMint.yourFollows.description'
|
||||
)}
|
||||
{this.renderRadioOption(
|
||||
'later',
|
||||
'views.MintDiscovery.chooseLater',
|
||||
'views.MintDiscovery.chooseLater.description'
|
||||
)}
|
||||
|
||||
{discoverMode === 'custom' && (
|
||||
<View style={styles.npubInputContainer}>
|
||||
<Text
|
||||
style={{
|
||||
...styles.npubLabel,
|
||||
color: themeColor('secondaryText')
|
||||
}}
|
||||
>
|
||||
{localeString('views.Cashu.AddMint.enterNpub')}
|
||||
</Text>
|
||||
<TextInput
|
||||
placeholder="npub1..."
|
||||
value={customNpub}
|
||||
onChangeText={this.validateAndSetNpub}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
error={!!npubError}
|
||||
textColor={
|
||||
npubError
|
||||
? themeColor('warning')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{npubError ? (
|
||||
<Text
|
||||
style={{
|
||||
...styles.npubError,
|
||||
color: themeColor('warning')
|
||||
}}
|
||||
>
|
||||
{npubError}
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.MintDiscovery.fetchMints'
|
||||
)}
|
||||
onPress={this.handleFetchMints}
|
||||
disabled={this.isFetchDisabled()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Top mints section */}
|
||||
{hasFetched && discoverMode !== 'later' && (
|
||||
<View style={styles.mintsSection}>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
fontSize: 16,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{localeString('views.MintDiscovery.topMints')}
|
||||
</Text>
|
||||
|
||||
{isLoading ? (
|
||||
<View style={{ padding: 20 }}>
|
||||
<LoadingIndicator />
|
||||
</View>
|
||||
) : topMints.length > 0 ? (
|
||||
topMints.map((mint: ScoredMint) => {
|
||||
const mintInfo =
|
||||
CashuStore.mintInfoCache.get(mint.url);
|
||||
const iconUrl = mintInfo?.icon_url;
|
||||
const mintName = mintInfo?.name;
|
||||
|
||||
return (
|
||||
<View
|
||||
key={mint.url}
|
||||
style={[
|
||||
styles.mintItem,
|
||||
{
|
||||
backgroundColor:
|
||||
themeColor('secondary'),
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<MintAvatar
|
||||
iconUrl={iconUrl}
|
||||
name={mintName || mint.url}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
marginLeft: 12
|
||||
}}
|
||||
>
|
||||
{mintName && (
|
||||
<RNText
|
||||
style={{
|
||||
color: themeColor(
|
||||
'text'
|
||||
),
|
||||
fontFamily:
|
||||
'PPNeueMontreal-Medium',
|
||||
fontSize: 14,
|
||||
marginBottom: 2
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{mintName}
|
||||
</RNText>
|
||||
)}
|
||||
<RNText
|
||||
style={{
|
||||
color: themeColor(
|
||||
'secondaryText'
|
||||
),
|
||||
fontFamily:
|
||||
'PPNeueMontreal-Book',
|
||||
fontSize: 12
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{mint.url.replace(
|
||||
'https://',
|
||||
''
|
||||
)}
|
||||
</RNText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
padding: 20
|
||||
}}
|
||||
>
|
||||
{CashuStore.error
|
||||
? CashuStore.error_msg
|
||||
: localeString(
|
||||
'views.RecommendedSettings.loadingMints'
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
padding: 20,
|
||||
paddingBottom: 40
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={localeString('views.MintDiscovery.next')}
|
||||
onPress={this.handleNext}
|
||||
disabled={isLoading && discoverMode !== 'later'}
|
||||
/>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
optionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
padding: 15,
|
||||
borderRadius: 10,
|
||||
marginBottom: 10
|
||||
},
|
||||
optionItemSelected: {
|
||||
borderWidth: 2
|
||||
},
|
||||
radioOuter: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
borderWidth: 2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
marginTop: 2
|
||||
},
|
||||
radioInner: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6
|
||||
},
|
||||
optionTextContainer: {
|
||||
flex: 1
|
||||
},
|
||||
optionTitle: {
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
fontSize: 16,
|
||||
marginBottom: 4
|
||||
},
|
||||
optionDescription: {
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 13
|
||||
},
|
||||
npubInputContainer: {
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 5
|
||||
},
|
||||
npubLabel: {
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 14,
|
||||
marginBottom: 8
|
||||
},
|
||||
npubError: {
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 12,
|
||||
marginTop: 5
|
||||
},
|
||||
mintsSection: {
|
||||
marginTop: 25,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#333',
|
||||
paddingTop: 15
|
||||
},
|
||||
mintItem: {
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ImageBackground,
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Route } from '@react-navigation/native';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import ModalStore from '../../stores/ModalStore';
|
||||
|
||||
import { font } from '../../utils/FontUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
interface NodeChoiceProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
ModalStore: ModalStore;
|
||||
route: Route<'NodeChoice', { enableCashu: boolean; returnTo?: string }>;
|
||||
}
|
||||
|
||||
@inject('ModalStore')
|
||||
@observer
|
||||
export default class NodeChoice extends React.Component<NodeChoiceProps, {}> {
|
||||
showLdkNodeInfo = () => {
|
||||
const { ModalStore } = this.props;
|
||||
ModalStore!.toggleInfoModal({
|
||||
text: [
|
||||
localeString('views.NodeChoice.ldkNode.info1'),
|
||||
localeString('views.NodeChoice.ldkNode.info2'),
|
||||
localeString('views.NodeChoice.ldkNode.info3')
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
showEmbeddedLndInfo = () => {
|
||||
const { ModalStore } = this.props;
|
||||
ModalStore!.toggleInfoModal({
|
||||
text: [
|
||||
localeString('views.NodeChoice.embeddedLnd.info1'),
|
||||
localeString('views.NodeChoice.embeddedLnd.info2'),
|
||||
localeString('views.NodeChoice.embeddedLnd.info3')
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
handleSelect = (implementation: 'ldk-node' | 'embedded-lnd') => {
|
||||
const { navigation, route } = this.props;
|
||||
const enableCashu = route.params?.enableCashu ?? false;
|
||||
const returnTo = route.params?.returnTo;
|
||||
|
||||
if (returnTo) {
|
||||
navigation.navigate(returnTo, { enableCashu, implementation });
|
||||
} else {
|
||||
navigation.navigate('RecommendedSettings', {
|
||||
enableCashu,
|
||||
implementation
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<ImageBackground
|
||||
source={require('../../assets/images/onboarding/eagle.jpeg')}
|
||||
resizeMode="cover"
|
||||
style={styles.backgroundImage}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Header leftComponent="Back" navigation={navigation} />
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontFamily: font('marlideBold'),
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.NodeChoice.whatMatters'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonsContainer}>
|
||||
<View style={styles.optionContainer}>
|
||||
<View style={styles.buttonWithInfo}>
|
||||
<View style={styles.buttonFlex}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.NodeChoice.speedReliability'
|
||||
)}
|
||||
onPress={() =>
|
||||
this.handleSelect(
|
||||
'ldk-node'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.showLdkNodeInfo}
|
||||
style={styles.infoButton}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: themeColor('text'),
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{' ?'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.optionContainer}>
|
||||
<View style={styles.buttonWithInfo}>
|
||||
<View style={styles.buttonFlex}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.NodeChoice.privacyPower'
|
||||
)}
|
||||
onPress={() =>
|
||||
this.handleSelect(
|
||||
'embedded-lnd'
|
||||
)
|
||||
}
|
||||
secondary
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.showEmbeddedLndInfo}
|
||||
style={styles.infoButton}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: themeColor('text'),
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{' ?'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
backgroundImage: {
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 40
|
||||
},
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
buttonsContainer: {
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 20
|
||||
},
|
||||
optionContainer: {
|
||||
padding: 10
|
||||
},
|
||||
buttonWithInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
buttonFlex: {
|
||||
flex: 1
|
||||
},
|
||||
infoButton: {
|
||||
padding: 10,
|
||||
marginLeft: 5
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
import { ImageBackground, View, StyleSheet } from 'react-native';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import { font } from '../../utils/FontUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
interface Onboarding1Props {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class Onboarding1 extends React.Component<Onboarding1Props, {}> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<ImageBackground
|
||||
source={require('../../assets/images/onboarding/zeus.jpeg')}
|
||||
resizeMode="cover"
|
||||
style={styles.backgroundImage}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Header leftComponent="Back" navigation={navigation} />
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontFamily: font('marlideBold'),
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.Onboarding.setupQuestion'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonsContainer}>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.Onboarding.recommended'
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate(
|
||||
'RecommendedSettings'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.Onboarding.questionnaire'
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate('WalletSetup')
|
||||
}
|
||||
secondary
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
backgroundImage: {
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 40
|
||||
},
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
buttonsContainer: {
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 20
|
||||
},
|
||||
buttonWrapper: {
|
||||
padding: 10
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,635 @@
|
||||
import * as React from 'react';
|
||||
import { View, ScrollView, Dimensions } from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Route } from '@react-navigation/native';
|
||||
import { Icon, ListItem } from '@rneui/themed';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import LoadingIndicator from '../../components/LoadingIndicator';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import CashuStore, { ScoredMint } from '../../stores/CashuStore';
|
||||
import SettingsStore, {
|
||||
CURRENCY_KEYS,
|
||||
DEFAULT_FIAT,
|
||||
DEFAULT_FIAT_RATES_SOURCE
|
||||
} from '../../stores/SettingsStore';
|
||||
|
||||
import { createOnboardingWallet } from '../../utils/WalletCreationUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
import MintAvatar from '../../components/MintAvatar';
|
||||
import Wordmark from '../../assets/images/SVG/wordmark-black.svg';
|
||||
|
||||
type DiscoverMode = 'zeus' | 'all' | 'custom' | 'later';
|
||||
|
||||
interface RecommendedSettingsProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
SettingsStore: SettingsStore;
|
||||
CashuStore: CashuStore;
|
||||
route: Route<
|
||||
'RecommendedSettings',
|
||||
{
|
||||
enableCashu?: boolean;
|
||||
discoverMode?: DiscoverMode;
|
||||
initialMintUrls?: string[];
|
||||
clipboard?: boolean;
|
||||
fiatEnabled?: boolean;
|
||||
selectedCurrency?: string;
|
||||
fiatRatesSource?: string;
|
||||
implementation?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface RecommendedSettingsState {
|
||||
enableCashu: boolean;
|
||||
discoverMode: DiscoverMode;
|
||||
clipboard: boolean;
|
||||
fiatEnabled: boolean;
|
||||
selectedCurrency: string;
|
||||
fiatRatesSource: string;
|
||||
initialMintUrls: string[];
|
||||
implementation: string;
|
||||
choosingPeers: boolean;
|
||||
creatingWallet: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
@inject('SettingsStore', 'CashuStore')
|
||||
@observer
|
||||
export default class RecommendedSettings extends React.Component<
|
||||
RecommendedSettingsProps,
|
||||
RecommendedSettingsState
|
||||
> {
|
||||
state: RecommendedSettingsState = {
|
||||
enableCashu: true,
|
||||
discoverMode: 'zeus',
|
||||
clipboard: true,
|
||||
fiatEnabled: true,
|
||||
selectedCurrency: DEFAULT_FIAT,
|
||||
fiatRatesSource: DEFAULT_FIAT_RATES_SOURCE,
|
||||
initialMintUrls: [],
|
||||
implementation: 'ldk-node',
|
||||
choosingPeers: false,
|
||||
creatingWallet: false,
|
||||
error: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { navigation, CashuStore, SettingsStore } = this.props;
|
||||
|
||||
// Fetch mints with default mode (ZEUS' contacts)
|
||||
CashuStore.fetchMintsFromFollows();
|
||||
|
||||
// Listen for updates when returning from questionnaire steps
|
||||
navigation.addListener('focus', async () => {
|
||||
this.syncFromRouteParams();
|
||||
|
||||
// Sync settings saved to the store by sub-screens
|
||||
const settings = await SettingsStore.getSettings();
|
||||
this.setState({
|
||||
clipboard: settings.privacy?.clipboard ?? true,
|
||||
fiatEnabled: settings.fiatEnabled ?? true,
|
||||
selectedCurrency: settings.fiat || DEFAULT_FIAT,
|
||||
fiatRatesSource:
|
||||
settings.fiatRatesSource || DEFAULT_FIAT_RATES_SOURCE
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
syncFromRouteParams = () => {
|
||||
const { route } = this.props;
|
||||
const params = route.params;
|
||||
if (!params) return;
|
||||
|
||||
if (params.enableCashu !== undefined) {
|
||||
this.setState({ enableCashu: params.enableCashu });
|
||||
}
|
||||
if (params.discoverMode) {
|
||||
this.setState({ discoverMode: params.discoverMode });
|
||||
}
|
||||
if (params.initialMintUrls) {
|
||||
this.setState({ initialMintUrls: params.initialMintUrls });
|
||||
}
|
||||
if (params.clipboard !== undefined) {
|
||||
this.setState({ clipboard: params.clipboard });
|
||||
}
|
||||
if (params.fiatEnabled !== undefined) {
|
||||
this.setState({ fiatEnabled: params.fiatEnabled });
|
||||
}
|
||||
if (params.selectedCurrency) {
|
||||
this.setState({ selectedCurrency: params.selectedCurrency });
|
||||
}
|
||||
if (params.fiatRatesSource) {
|
||||
this.setState({ fiatRatesSource: params.fiatRatesSource });
|
||||
}
|
||||
if (params.implementation) {
|
||||
this.setState({ implementation: params.implementation });
|
||||
}
|
||||
};
|
||||
|
||||
handleCreateWallet = async () => {
|
||||
const { navigation, SettingsStore, CashuStore } = this.props;
|
||||
const {
|
||||
enableCashu,
|
||||
discoverMode,
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
fiatRatesSource,
|
||||
initialMintUrls,
|
||||
implementation
|
||||
} = this.state;
|
||||
|
||||
// Use stored mint URLs or compute from top scored mints
|
||||
let mintUrls = initialMintUrls;
|
||||
if (enableCashu && mintUrls.length === 0) {
|
||||
const topMints = CashuStore.getTopScoredMints(
|
||||
5,
|
||||
discoverMode === 'all' ? 'all' : 'zeus'
|
||||
);
|
||||
mintUrls = topMints.map((m: ScoredMint) => m.url);
|
||||
}
|
||||
|
||||
await createOnboardingWallet({
|
||||
settingsStore: SettingsStore,
|
||||
enableCashu,
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
fiatRatesSource,
|
||||
initialMintUrls: enableCashu ? mintUrls : undefined,
|
||||
implementation,
|
||||
onChoosingPeers: () =>
|
||||
this.setState({ choosingPeers: true, error: false }),
|
||||
onCreatingWallet: () =>
|
||||
this.setState({
|
||||
choosingPeers: false,
|
||||
creatingWallet: true
|
||||
}),
|
||||
onError: () =>
|
||||
this.setState({
|
||||
error: true,
|
||||
choosingPeers: false,
|
||||
creatingWallet: false
|
||||
}),
|
||||
onSuccess: () => navigation.navigate('Wallet')
|
||||
});
|
||||
};
|
||||
|
||||
getMintSourceLabel = () => {
|
||||
const { discoverMode } = this.state;
|
||||
switch (discoverMode) {
|
||||
case 'zeus':
|
||||
return localeString('views.Cashu.AddMint.zeusFollows');
|
||||
case 'all':
|
||||
return localeString('views.Cashu.AddMint.allNostrUsers');
|
||||
case 'custom':
|
||||
return localeString('views.Cashu.AddMint.yourFollows');
|
||||
case 'later':
|
||||
return localeString('views.MintDiscovery.chooseLater');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, CashuStore } = this.props;
|
||||
const {
|
||||
enableCashu,
|
||||
discoverMode,
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
implementation,
|
||||
choosingPeers,
|
||||
creatingWallet,
|
||||
error
|
||||
} = this.state;
|
||||
|
||||
const currencyInfo = CURRENCY_KEYS.find(
|
||||
(c) => c.value === selectedCurrency
|
||||
);
|
||||
const currencyFlag = currencyInfo?.flag || '';
|
||||
const currencyLabel = currencyFlag
|
||||
? `${currencyFlag} ${selectedCurrency}`
|
||||
: selectedCurrency;
|
||||
|
||||
if (choosingPeers || creatingWallet) {
|
||||
return (
|
||||
<Screen>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
top: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: Dimensions.get('window').width * 0.85,
|
||||
maxHeight: 200,
|
||||
marginTop: 10,
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
>
|
||||
<Wordmark fill={themeColor('highlight')} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
alignSelf: 'center',
|
||||
fontSize: 15,
|
||||
padding: 8
|
||||
}}
|
||||
>
|
||||
{choosingPeers
|
||||
? localeString('views.Intro.choosingPeers')
|
||||
: localeString(
|
||||
'views.Intro.creatingWallet'
|
||||
).replace('Zeus', 'ZEUS')}
|
||||
</Text>
|
||||
<View style={{ marginTop: 40 }}>
|
||||
<LoadingIndicator />
|
||||
</View>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
const isLater = discoverMode === 'later';
|
||||
|
||||
const isLoadingMints = isLater
|
||||
? false
|
||||
: discoverMode === 'all'
|
||||
? CashuStore.loading
|
||||
: CashuStore.loadingTrustedMints;
|
||||
|
||||
const topMints =
|
||||
enableCashu && !isLater
|
||||
? CashuStore.getTopScoredMints(
|
||||
5,
|
||||
discoverMode === 'all' ? 'all' : 'zeus'
|
||||
)
|
||||
: [];
|
||||
|
||||
// Fetch mint info for icons when top mints are available
|
||||
if (topMints.length > 0 && !isLoadingMints) {
|
||||
const urls = topMints.map((m: ScoredMint) => m.url);
|
||||
const hasUncached = urls.some(
|
||||
(u: string) => !CashuStore.mintInfoCache.has(u)
|
||||
);
|
||||
if (hasUncached) {
|
||||
CashuStore.fetchMintInfoBatch(urls);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header
|
||||
leftComponent="Back"
|
||||
centerComponent={{
|
||||
text: localeString('views.RecommendedSettings.title'),
|
||||
style: {
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}
|
||||
}}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flex: 1, paddingHorizontal: 15, marginTop: 5 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{error && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: themeColor('error'),
|
||||
padding: 15,
|
||||
borderRadius: 5,
|
||||
marginBottom: 15
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.Intro.errorCreatingWallet'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Wallet type */}
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('WalletType', {
|
||||
returnTo: 'RecommendedSettings'
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.RecommendedSettings.walletType'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
marginTop: 4
|
||||
}}
|
||||
>
|
||||
{enableCashu
|
||||
? localeString(
|
||||
'views.RecommendedSettings.walletType.cashu'
|
||||
)
|
||||
: localeString(
|
||||
'views.RecommendedSettings.walletType.selfCustody'
|
||||
)}
|
||||
</ListItem.Subtitle>
|
||||
{enableCashu && (
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
fontSize: 12,
|
||||
marginTop: 2
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.RecommendedSettings.walletType.subtitle'
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{/* Node implementation */}
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator')
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('NodeChoice', {
|
||||
returnTo: 'RecommendedSettings',
|
||||
enableCashu
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.RecommendedSettings.nodeImplementation'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
marginTop: 4
|
||||
}}
|
||||
>
|
||||
{implementation === 'ldk-node'
|
||||
? `LDK Node (${localeString(
|
||||
'views.NodeChoice.speedReliability'
|
||||
).toLowerCase()})`
|
||||
: `Embedded LND (${localeString(
|
||||
'views.NodeChoice.privacyPower'
|
||||
).toLowerCase()})`}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{/* Mint source (only when ecash enabled) */}
|
||||
{enableCashu && (
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator')
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('MintDiscovery', {
|
||||
returnTo: 'RecommendedSettings',
|
||||
enableCashu: true
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.RecommendedSettings.mintSource'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
marginTop: 4
|
||||
}}
|
||||
>
|
||||
{this.getMintSourceLabel()}
|
||||
</ListItem.Subtitle>
|
||||
{isLoadingMints ? (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 10,
|
||||
height: 36,
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<LoadingIndicator size={24} />
|
||||
</View>
|
||||
) : topMints.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
gap: 8
|
||||
}}
|
||||
>
|
||||
{topMints.map((mint: ScoredMint) => {
|
||||
const mintInfo =
|
||||
CashuStore.mintInfoCache.get(
|
||||
mint.url
|
||||
);
|
||||
return (
|
||||
<MintAvatar
|
||||
key={mint.url}
|
||||
iconUrl={mintInfo?.icon_url}
|
||||
name={
|
||||
mintInfo?.name ||
|
||||
mint.url
|
||||
}
|
||||
size="medium"
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : null}
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{/* Clipboard */}
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator')
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('WalletSettings', {
|
||||
returnTo: 'RecommendedSettings',
|
||||
enableCashu
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.RecommendedSettings.clipboard'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
marginTop: 4
|
||||
}}
|
||||
>
|
||||
{clipboard
|
||||
? localeString(
|
||||
'views.Settings.Privacy.clipboard'
|
||||
)
|
||||
: localeString(
|
||||
'views.RecommendedSettings.off'
|
||||
)}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{/* Fiat */}
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator')
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate('WalletSettings', {
|
||||
returnTo: 'RecommendedSettings',
|
||||
enableCashu
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString('views.RecommendedSettings.fiat')}
|
||||
</ListItem.Title>
|
||||
<ListItem.Subtitle
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Medium',
|
||||
marginTop: 4
|
||||
}}
|
||||
>
|
||||
{fiatEnabled
|
||||
? currencyLabel
|
||||
: localeString(
|
||||
'views.RecommendedSettings.disabled'
|
||||
)}
|
||||
</ListItem.Subtitle>
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
padding: 20,
|
||||
paddingBottom: 40
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.RecommendedSettings.createWallet'
|
||||
)}
|
||||
onPress={this.handleCreateWallet}
|
||||
disabled={isLoadingMints}
|
||||
/>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import * as React from 'react';
|
||||
import { View, ScrollView, Dimensions } from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Route } from '@react-navigation/native';
|
||||
import { Icon, ListItem } from '@rneui/themed';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import LoadingIndicator from '../../components/LoadingIndicator';
|
||||
import Screen from '../../components/Screen';
|
||||
import Switch from '../../components/Switch';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import SettingsStore, {
|
||||
CURRENCY_KEYS,
|
||||
DEFAULT_FIAT,
|
||||
DEFAULT_FIAT_RATES_SOURCE,
|
||||
FIAT_RATES_SOURCE_KEYS
|
||||
} from '../../stores/SettingsStore';
|
||||
|
||||
import { createOnboardingWallet } from '../../utils/WalletCreationUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
import Wordmark from '../../assets/images/SVG/wordmark-black.svg';
|
||||
import DropdownSetting from '../../components/DropdownSetting';
|
||||
|
||||
interface WalletSettingsProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
SettingsStore: SettingsStore;
|
||||
route: Route<
|
||||
'WalletSettings',
|
||||
{
|
||||
enableCashu: boolean;
|
||||
returnTo?: string;
|
||||
initialMintUrls?: string[];
|
||||
implementation?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
interface WalletSettingsState {
|
||||
clipboard: boolean;
|
||||
fiatEnabled: boolean;
|
||||
selectedCurrency: string;
|
||||
fiatRatesSource: string;
|
||||
choosingPeers: boolean;
|
||||
creatingWallet: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
@inject('SettingsStore')
|
||||
@observer
|
||||
export default class WalletSettings extends React.Component<
|
||||
WalletSettingsProps,
|
||||
WalletSettingsState
|
||||
> {
|
||||
state = {
|
||||
clipboard: true,
|
||||
fiatEnabled: true,
|
||||
selectedCurrency: DEFAULT_FIAT,
|
||||
fiatRatesSource: DEFAULT_FIAT_RATES_SOURCE,
|
||||
choosingPeers: false,
|
||||
creatingWallet: false,
|
||||
error: false
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const { navigation, SettingsStore } = this.props;
|
||||
const settings = await SettingsStore.getSettings();
|
||||
|
||||
this.setState({
|
||||
clipboard: settings.privacy?.clipboard ?? true,
|
||||
fiatEnabled: settings.fiatEnabled ?? true,
|
||||
selectedCurrency: settings.fiat || DEFAULT_FIAT,
|
||||
fiatRatesSource:
|
||||
settings.fiatRatesSource || DEFAULT_FIAT_RATES_SOURCE
|
||||
});
|
||||
|
||||
// Update currency when returning from SelectCurrency
|
||||
navigation.addListener('focus', async () => {
|
||||
const updatedSettings = await SettingsStore.getSettings();
|
||||
if (updatedSettings.fiat !== this.state.selectedCurrency) {
|
||||
this.setState({
|
||||
selectedCurrency: updatedSettings.fiat || DEFAULT_FIAT
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateWallet = async () => {
|
||||
const { navigation, SettingsStore, route } = this.props;
|
||||
const { clipboard, fiatEnabled, selectedCurrency, fiatRatesSource } =
|
||||
this.state;
|
||||
const enableCashu = route.params?.enableCashu || false;
|
||||
const initialMintUrls = route.params?.initialMintUrls;
|
||||
const implementation = route.params?.implementation;
|
||||
|
||||
await createOnboardingWallet({
|
||||
settingsStore: SettingsStore,
|
||||
enableCashu,
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
fiatRatesSource,
|
||||
initialMintUrls,
|
||||
implementation,
|
||||
onChoosingPeers: () =>
|
||||
this.setState({ choosingPeers: true, error: false }),
|
||||
onCreatingWallet: () =>
|
||||
this.setState({
|
||||
choosingPeers: false,
|
||||
creatingWallet: true
|
||||
}),
|
||||
onError: () =>
|
||||
this.setState({
|
||||
error: true,
|
||||
choosingPeers: false,
|
||||
creatingWallet: false
|
||||
}),
|
||||
onSuccess: () => navigation.navigate('Wallet')
|
||||
});
|
||||
};
|
||||
|
||||
navigateToSelectCurrency = () => {
|
||||
this.props.navigation.navigate('SelectCurrency');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const {
|
||||
clipboard,
|
||||
fiatEnabled,
|
||||
selectedCurrency,
|
||||
fiatRatesSource,
|
||||
choosingPeers,
|
||||
creatingWallet,
|
||||
error
|
||||
} = this.state;
|
||||
|
||||
if (choosingPeers || creatingWallet) {
|
||||
return (
|
||||
<Screen>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
top: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: Dimensions.get('window').width * 0.85,
|
||||
maxHeight: 200,
|
||||
marginTop: 10,
|
||||
alignSelf: 'center'
|
||||
}}
|
||||
>
|
||||
<Wordmark fill={themeColor('highlight')} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
alignSelf: 'center',
|
||||
fontSize: 15,
|
||||
padding: 8
|
||||
}}
|
||||
>
|
||||
{choosingPeers
|
||||
? localeString('views.Intro.choosingPeers')
|
||||
: localeString(
|
||||
'views.Intro.creatingWallet'
|
||||
).replace('Zeus', 'ZEUS')}
|
||||
</Text>
|
||||
<View style={{ marginTop: 40 }}>
|
||||
<LoadingIndicator />
|
||||
</View>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<Header
|
||||
leftComponent="Back"
|
||||
centerComponent={{
|
||||
text: localeString('views.WalletSettings.title'),
|
||||
style: {
|
||||
color: themeColor('text'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}
|
||||
}}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<ScrollView
|
||||
style={{ flex: 1, paddingHorizontal: 15, marginTop: 5 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{error && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: themeColor('error'),
|
||||
padding: 15,
|
||||
borderRadius: 5,
|
||||
marginBottom: 15
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.Intro.errorCreatingWallet'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontSize: 17,
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
infoModalText={localeString(
|
||||
'views.Settings.Privacy.clipboard.explainer'
|
||||
).replace('Zeus', 'ZEUS')}
|
||||
>
|
||||
{localeString(
|
||||
'views.Settings.Privacy.clipboard'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ alignSelf: 'center', marginLeft: 5 }}>
|
||||
<Switch
|
||||
value={clipboard}
|
||||
onValueChange={() => {
|
||||
const newValue = !clipboard;
|
||||
this.setState({ clipboard: newValue });
|
||||
this.props.SettingsStore.updateSettings({
|
||||
privacy: { clipboard: newValue }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 30,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator'),
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
borderBottomWidth: 0,
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.Settings.Currency.enableFiat'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={fiatEnabled}
|
||||
onValueChange={() => {
|
||||
const newValue = !fiatEnabled;
|
||||
this.setState({
|
||||
fiatEnabled: newValue
|
||||
});
|
||||
this.props.SettingsStore.updateSettings(
|
||||
{ fiatEnabled: newValue }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</ListItem>
|
||||
|
||||
{fiatEnabled && (
|
||||
<>
|
||||
<DropdownSetting
|
||||
title={
|
||||
localeString(
|
||||
'views.Settings.Currency.source'
|
||||
) + ':'
|
||||
}
|
||||
selectedValue={fiatRatesSource}
|
||||
onValueChange={(value: string) => {
|
||||
this.setState({
|
||||
fiatRatesSource: value
|
||||
});
|
||||
if (
|
||||
!CURRENCY_KEYS.find(
|
||||
(c) =>
|
||||
c.value === selectedCurrency
|
||||
)?.supportedSources.includes(value)
|
||||
) {
|
||||
this.setState({
|
||||
selectedCurrency: DEFAULT_FIAT
|
||||
});
|
||||
}
|
||||
}}
|
||||
values={FIAT_RATES_SOURCE_KEYS}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={this.navigateToSelectCurrency}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor(
|
||||
'secondaryText'
|
||||
),
|
||||
fontFamily:
|
||||
'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const currencyInfo =
|
||||
CURRENCY_KEYS.find(
|
||||
(c) =>
|
||||
c.value ===
|
||||
selectedCurrency
|
||||
);
|
||||
const flag =
|
||||
currencyInfo?.flag || '';
|
||||
return (
|
||||
localeString(
|
||||
'views.Settings.Currency.selectCurrency'
|
||||
) +
|
||||
` (${
|
||||
flag ? `${flag} ` : ''
|
||||
}${selectedCurrency})`
|
||||
);
|
||||
})()}
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 30,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: themeColor('separator'),
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
containerStyle={{
|
||||
backgroundColor: 'transparent',
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => navigation.navigate('Security')}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<ListItem.Title
|
||||
style={{
|
||||
color: themeColor('secondaryText'),
|
||||
fontFamily: 'PPNeueMontreal-Book'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.Settings.Security.title'
|
||||
)}
|
||||
</ListItem.Title>
|
||||
</ListItem.Content>
|
||||
<Icon
|
||||
name="keyboard-arrow-right"
|
||||
color={themeColor('secondaryText')}
|
||||
/>
|
||||
</ListItem>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
padding: 20,
|
||||
paddingBottom: 40
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.WalletSettings.createWallet'
|
||||
)}
|
||||
onPress={this.handleCreateWallet}
|
||||
/>
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import * as React from 'react';
|
||||
import { ImageBackground, View, StyleSheet } from 'react-native';
|
||||
import { observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import { font } from '../../utils/FontUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
interface WalletSetupProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class WalletSetup extends React.Component<WalletSetupProps, {}> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<ImageBackground
|
||||
source={require('../../assets/images/onboarding/temple.jpeg')}
|
||||
resizeMode="cover"
|
||||
style={styles.backgroundImage}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Header leftComponent="Back" navigation={navigation} />
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontFamily: font('marlideBold'),
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.WalletSetup.howToUseZeus'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonsContainer}>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.WalletSetup.createNewWallet'
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate('WalletType')
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonWrapper}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.WalletSetup.connectRemoteNode'
|
||||
)}
|
||||
onPress={() =>
|
||||
navigation.navigate(
|
||||
'WalletConfiguration',
|
||||
{ newEntry: true }
|
||||
)
|
||||
}
|
||||
secondary
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
backgroundImage: {
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 40
|
||||
},
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
buttonsContainer: {
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 20
|
||||
},
|
||||
buttonWrapper: {
|
||||
padding: 10
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ImageBackground,
|
||||
View,
|
||||
StyleSheet,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Route } from '@react-navigation/native';
|
||||
|
||||
import Button from '../../components/Button';
|
||||
import Header from '../../components/Header';
|
||||
import Screen from '../../components/Screen';
|
||||
import Text from '../../components/Text';
|
||||
|
||||
import ModalStore from '../../stores/ModalStore';
|
||||
|
||||
import { font } from '../../utils/FontUtils';
|
||||
import { localeString } from '../../utils/LocaleUtils';
|
||||
import { themeColor } from '../../utils/ThemeUtils';
|
||||
|
||||
interface WalletTypeProps {
|
||||
navigation: NativeStackNavigationProp<any, any>;
|
||||
ModalStore: ModalStore;
|
||||
route: Route<'WalletType', { returnTo?: string }>;
|
||||
}
|
||||
|
||||
@inject('ModalStore')
|
||||
@observer
|
||||
export default class WalletType extends React.Component<WalletTypeProps, {}> {
|
||||
showSelfCustodyInfo = () => {
|
||||
const { ModalStore } = this.props;
|
||||
ModalStore!.toggleInfoModal({
|
||||
text: [
|
||||
localeString('views.WalletType.selfCustody.info1'),
|
||||
localeString('views.WalletType.selfCustody.info2'),
|
||||
localeString('views.WalletType.selfCustody.info3')
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
showCashuInfo = () => {
|
||||
const { ModalStore } = this.props;
|
||||
ModalStore!.toggleInfoModal({
|
||||
text: [
|
||||
localeString('views.WalletType.cashu.info1'),
|
||||
localeString('views.WalletType.cashu.info2'),
|
||||
localeString('views.WalletType.cashu.info3')
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
handleSelect = (enableCashu: boolean) => {
|
||||
const { navigation, route } = this.props;
|
||||
const returnTo = route.params?.returnTo;
|
||||
|
||||
if (returnTo) {
|
||||
navigation.navigate(returnTo, { enableCashu });
|
||||
} else {
|
||||
navigation.navigate('NodeChoice', { enableCashu });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<ImageBackground
|
||||
source={require('../../assets/images/onboarding/journey.jpeg')}
|
||||
resizeMode="cover"
|
||||
style={styles.backgroundImage}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Header leftComponent="Back" navigation={navigation} />
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 32,
|
||||
fontFamily: font('marlideBold'),
|
||||
color: themeColor('text'),
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{localeString(
|
||||
'views.WalletType.whatsImportant'
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonsContainer}>
|
||||
<View style={styles.optionContainer}>
|
||||
<View style={styles.buttonWithInfo}>
|
||||
<View style={styles.buttonFlex}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.WalletType.control'
|
||||
)}
|
||||
onPress={() =>
|
||||
this.handleSelect(false)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.showSelfCustodyInfo}
|
||||
style={styles.infoButton}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: themeColor('text'),
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{' ?'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.optionContainer}>
|
||||
<View style={styles.buttonWithInfo}>
|
||||
<View style={styles.buttonFlex}>
|
||||
<Button
|
||||
title={localeString(
|
||||
'views.WalletType.lowFees'
|
||||
)}
|
||||
onPress={() =>
|
||||
this.handleSelect(true)
|
||||
}
|
||||
secondary
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={this.showCashuInfo}
|
||||
style={styles.infoButton}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: themeColor('text'),
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{' ?'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
backgroundImage: {
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 40
|
||||
},
|
||||
headerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
buttonsContainer: {
|
||||
paddingHorizontal: 10,
|
||||
paddingBottom: 20
|
||||
},
|
||||
optionContainer: {
|
||||
padding: 10
|
||||
},
|
||||
buttonWithInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
buttonFlex: {
|
||||
flex: 1
|
||||
},
|
||||
infoButton: {
|
||||
padding: 10,
|
||||
marginLeft: 5
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user