feat: upgrade to self-custody prompts
This commit is contained in:
@@ -109,11 +109,11 @@ export default class KeyValue extends React.Component<KeyValueProps, {}> {
|
||||
Key = (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
toggleInfoModal(
|
||||
infoModalText,
|
||||
infoModalLink,
|
||||
infoModalAdditionalButtons
|
||||
)
|
||||
toggleInfoModal({
|
||||
text: infoModalText,
|
||||
link: infoModalLink,
|
||||
buttons: infoModalAdditionalButtons
|
||||
})
|
||||
}
|
||||
>
|
||||
{KeyBase}
|
||||
|
||||
@@ -271,9 +271,9 @@ export default class LightningSwipeableRow extends Component<
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
stores.modalStore.toggleInfoModal(
|
||||
localeString('views.Wallet.waitForSync')
|
||||
)
|
||||
stores.modalStore.toggleInfoModal({
|
||||
text: localeString('views.Wallet.waitForSync')
|
||||
})
|
||||
}
|
||||
>
|
||||
<View style={{ opacity: 0.25 }}>{children}</View>
|
||||
|
||||
@@ -185,9 +185,9 @@ export default class OnchainSwipeableRow extends Component<
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
stores.modalStore.toggleInfoModal(
|
||||
localeString('views.Wallet.waitForSync')
|
||||
)
|
||||
stores.modalStore.toggleInfoModal({
|
||||
text: localeString('views.Wallet.waitForSync')
|
||||
})
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
|
||||
@@ -22,12 +22,15 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
const { ModalStore } = this.props;
|
||||
const {
|
||||
showInfoModal,
|
||||
infoModalTitle,
|
||||
infoModalText,
|
||||
infoModalLink,
|
||||
infoModalAdditionalButtons,
|
||||
toggleInfoModal
|
||||
} = ModalStore;
|
||||
|
||||
const additionalButtonsCount = infoModalAdditionalButtons?.length || 0;
|
||||
|
||||
return (
|
||||
<ModalBox
|
||||
isOpen={showInfoModal}
|
||||
@@ -35,7 +38,7 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
backgroundColor: 'transparent',
|
||||
minHeight: 200
|
||||
}}
|
||||
onClosed={() => toggleInfoModal()}
|
||||
onClosed={() => toggleInfoModal({})}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -57,6 +60,20 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{infoModalTitle && (
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: 'MarlideDisplay_Bold',
|
||||
color: themeColor('text'),
|
||||
fontSize: 34,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
{infoModalTitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{typeof infoModalText === 'string' && (
|
||||
<Text
|
||||
style={{
|
||||
@@ -77,10 +94,8 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
style={{
|
||||
fontFamily: 'PPNeueMontreal-Book',
|
||||
color: themeColor('text'),
|
||||
fontSize:
|
||||
infoModalText.length > 3 ? 18 : 20,
|
||||
marginBottom:
|
||||
infoModalText.length > 3 ? 18 : 20
|
||||
fontSize: 18,
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
@@ -100,10 +115,12 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
<Button
|
||||
title={title}
|
||||
onPress={() => {
|
||||
toggleInfoModal();
|
||||
toggleInfoModal({});
|
||||
if (callback) callback();
|
||||
}}
|
||||
tertiary
|
||||
// index 0 = primary (default)
|
||||
tertiary={index === 1}
|
||||
quaternary={index >= 1}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
@@ -120,17 +137,21 @@ export default class InfoModal extends React.Component<InfoModalProps, {}> {
|
||||
'general.learnMore'
|
||||
)}
|
||||
onPress={() => {
|
||||
toggleInfoModal();
|
||||
toggleInfoModal({});
|
||||
UrlUtils.goToUrl(infoModalLink);
|
||||
}}
|
||||
tertiary
|
||||
// Style based on the count of preceding additional buttons
|
||||
// additionalButtonsCount 0 => index 0 (primary)
|
||||
secondary={additionalButtonsCount === 1} // index 1 = secondary
|
||||
tertiary={additionalButtonsCount === 2} // index 2 = tertiary
|
||||
quaternary={additionalButtonsCount >= 3} // index 3+ = quaternary
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.button}>
|
||||
<Button
|
||||
title={localeString('general.close')}
|
||||
onPress={() => toggleInfoModal()}
|
||||
onPress={() => toggleInfoModal({})}
|
||||
secondary
|
||||
/>
|
||||
</View>
|
||||
|
||||
+5
-5
@@ -59,11 +59,11 @@ export default class ZeusText extends React.Component<TextProps, {}> {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
toggleInfoModal(
|
||||
infoModalText,
|
||||
infoModalLink,
|
||||
infoModalAdditionalButtons
|
||||
)
|
||||
toggleInfoModal({
|
||||
text: infoModalText,
|
||||
link: infoModalLink,
|
||||
buttons: infoModalAdditionalButtons
|
||||
})
|
||||
}
|
||||
>
|
||||
<CoreText />
|
||||
|
||||
+9
-1
@@ -1479,6 +1479,14 @@
|
||||
"stores.CashuStore.errorMintingToken": "Error minting token",
|
||||
"stores.CashuStore.notEnoughFunds": "Not enough funds to cover payment",
|
||||
"stores.CashuStore.feeExceedsAmt": "Melt fees match or exceed token amount",
|
||||
"cashu.upgradePrompt.title": "Upgrade to Self-Custody?",
|
||||
"cashu.upgradePrompt.messageGeneral": "Your ecash balance is growing! Consider upgrading to a self-custodial Lightning wallet for better security and control over your funds.",
|
||||
"cashu.upgradePrompt.message10k": "You now have enough sats to purchase your first Lightning channel! Upgrade to self-custody to truly own your bitcoin.",
|
||||
"cashu.upgradePrompt.message25k": "Your ecash balance is growing! For better security, consider upgrading to a self-custodial Lightning wallet.",
|
||||
"cashu.upgradePrompt.message50k": "With this balance, it's strongly recommended to move to a self-custodial Lightning wallet. Don't trust, verify!",
|
||||
"cashu.upgradePrompt.message100k": "High balance detected! It's crucial to upgrade to a self-custodial Lightning wallet now to secure your funds.",
|
||||
"cashu.upgradePrompt.purchaseChannel": "Purchase Channel",
|
||||
"cashu.upgradePrompt.learnMore": "Learn About Self-Custody",
|
||||
"zeuspay.intro.explainer1": "ZEUS Pay is a service that give you a Lightning address, like satoshi@zeuspay.com, for receiving payments to your Lightning wallet.",
|
||||
"zeuspay.intro.explainer2": "Below are the kinds of Lightning addresses you can have for your wallet type:",
|
||||
"zeuspay.intro.prosAndCons": "Pros and cons",
|
||||
@@ -1500,4 +1508,4 @@
|
||||
"zeuspay.getChannel": "Get channel",
|
||||
"androidNotification.lndRunningBackground": "LND is running in the background",
|
||||
"androidNotification.shutdown": "Shutdown"
|
||||
}
|
||||
}
|
||||
|
||||
+86
-5
@@ -30,12 +30,16 @@ import Storage from '../storage';
|
||||
|
||||
import stores from './Stores';
|
||||
import InvoicesStore from './InvoicesStore';
|
||||
import ChannelsStore from './ChannelsStore';
|
||||
import SettingsStore, { DEFAULT_NOSTR_RELAYS } from './SettingsStore';
|
||||
import ModalStore from './ModalStore';
|
||||
|
||||
import Base64Utils from '../utils/Base64Utils';
|
||||
import CashuUtils from '../utils/CashuUtils';
|
||||
import { localeString } from '../utils/LocaleUtils';
|
||||
import { errorToUserFriendly } from '../utils/ErrorUtils';
|
||||
import UrlUtils from '../utils/UrlUtils';
|
||||
import NavigationService from '../NavigationService';
|
||||
|
||||
const bip39 = require('bip39');
|
||||
|
||||
@@ -43,6 +47,14 @@ const BATCH_SIZE = 100;
|
||||
const MAX_GAP = 3;
|
||||
const RESTORE_PROOFS_EVENT_NAME = 'RESTORING_PROOF_EVENT';
|
||||
|
||||
const UPGRADE_THRESHOLDS = [10000, 25000, 50000, 100000];
|
||||
const UPGRADE_MESSAGES: { [key: number]: string } = {
|
||||
10000: 'cashu.upgradePrompt.message10k',
|
||||
25000: 'cashu.upgradePrompt.message25k',
|
||||
50000: 'cashu.upgradePrompt.message50k',
|
||||
100000: 'cashu.upgradePrompt.message100k'
|
||||
};
|
||||
|
||||
interface Wallet {
|
||||
wallet?: CashuWallet;
|
||||
walletId: string;
|
||||
@@ -104,15 +116,25 @@ export default class CashuStore {
|
||||
|
||||
@observable public mintRecommendations?: MintRecommendation[];
|
||||
@observable loadingFeeEstimate = false;
|
||||
@observable shownThresholdModals: number[] = [];
|
||||
|
||||
settingsStore: SettingsStore;
|
||||
invoicesStore: InvoicesStore;
|
||||
channelsStore: ChannelsStore;
|
||||
modalStore: ModalStore;
|
||||
|
||||
ndk: NDK;
|
||||
|
||||
constructor(settingsStore: SettingsStore, invoicesStore: InvoicesStore) {
|
||||
constructor(
|
||||
settingsStore: SettingsStore,
|
||||
invoicesStore: InvoicesStore,
|
||||
channelsStore: ChannelsStore,
|
||||
modalStore: ModalStore
|
||||
) {
|
||||
this.settingsStore = settingsStore;
|
||||
this.invoicesStore = invoicesStore;
|
||||
this.channelsStore = channelsStore;
|
||||
this.modalStore = modalStore;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -123,6 +145,7 @@ export default class CashuStore {
|
||||
this.selectedMintUrl = '';
|
||||
this.clearInvoice();
|
||||
this.clearPayReq();
|
||||
this.shownThresholdModals = [];
|
||||
};
|
||||
|
||||
@action
|
||||
@@ -447,17 +470,75 @@ export default class CashuStore {
|
||||
return balanceSats;
|
||||
};
|
||||
|
||||
setTotalBalance = async (totalBalanceSats: number) => {
|
||||
setTotalBalance = async (newTotalBalanceSats: number) => {
|
||||
const previousBalance = this.totalBalanceSats || 0;
|
||||
await Storage.setItem(
|
||||
`${this.getLndDir()}-cashu-totalBalanceSats`,
|
||||
totalBalanceSats
|
||||
newTotalBalanceSats
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.totalBalanceSats = totalBalanceSats;
|
||||
this.totalBalanceSats = newTotalBalanceSats;
|
||||
});
|
||||
|
||||
return totalBalanceSats;
|
||||
// Check thresholds after balance update
|
||||
this.checkAndShowUpgradeModal(previousBalance, newTotalBalanceSats);
|
||||
|
||||
return newTotalBalanceSats;
|
||||
};
|
||||
|
||||
@action
|
||||
public checkAndShowUpgradeModal = (
|
||||
previousBalance: number,
|
||||
currentBalance: number
|
||||
) => {
|
||||
for (const threshold of UPGRADE_THRESHOLDS.reverse()) {
|
||||
if (
|
||||
previousBalance < threshold &&
|
||||
currentBalance >= threshold &&
|
||||
!this.shownThresholdModals.includes(threshold) &&
|
||||
this.channelsStore.channels.length === 0
|
||||
) {
|
||||
const messageKey =
|
||||
UPGRADE_MESSAGES[threshold] ||
|
||||
'cashu.upgradePrompt.messageGeneral'; // Fallback message
|
||||
const message = localeString(messageKey);
|
||||
const title = localeString('cashu.upgradePrompt.title');
|
||||
|
||||
this.modalStore.toggleInfoModal({
|
||||
title,
|
||||
text: message,
|
||||
buttons: [
|
||||
{
|
||||
title: localeString(
|
||||
'cashu.upgradePrompt.purchaseChannel'
|
||||
),
|
||||
callback: () => {
|
||||
this.modalStore.toggleInfoModal({}); // Close current modal first
|
||||
NavigationService.navigate('LSPS1');
|
||||
}
|
||||
},
|
||||
{
|
||||
title: localeString(
|
||||
'cashu.upgradePrompt.learnMore'
|
||||
),
|
||||
callback: () => {
|
||||
this.modalStore.toggleInfoModal({}); // Close current modal first
|
||||
UrlUtils.goToUrl(
|
||||
'https://docs.zeusln.app/category/self-custody'
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Mark this threshold modal as shown for the current session
|
||||
runInAction(() => {
|
||||
this.shownThresholdModals.push(threshold);
|
||||
});
|
||||
break; // Show only one modal per balance update
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
|
||||
+17
-8
@@ -7,6 +7,7 @@ export default class ModalStore {
|
||||
@observable public showAlertModal: boolean = false;
|
||||
@observable public modalUrl: string;
|
||||
@observable public clipboardValue: string;
|
||||
@observable public infoModalTitle: string | undefined;
|
||||
@observable public infoModalText: string | Array<string> | undefined;
|
||||
@observable public infoModalLink: string | undefined;
|
||||
@observable public infoModalAdditionalButtons?: Array<{
|
||||
@@ -25,12 +26,19 @@ export default class ModalStore {
|
||||
};
|
||||
|
||||
@action
|
||||
public toggleInfoModal = (
|
||||
text?: string | Array<string>,
|
||||
link?: string,
|
||||
buttons?: Array<{ title: string; callback?: () => void }>
|
||||
) => {
|
||||
this.showInfoModal = text ? true : false;
|
||||
public toggleInfoModal = ({
|
||||
title,
|
||||
text,
|
||||
link,
|
||||
buttons
|
||||
}: {
|
||||
title?: string;
|
||||
text?: string | Array<string>;
|
||||
link?: string;
|
||||
buttons?: Array<{ title: string; callback?: () => void }>;
|
||||
}) => {
|
||||
this.showInfoModal = title || text ? true : false; // Show if title or text exists
|
||||
this.infoModalTitle = title;
|
||||
this.infoModalText = text;
|
||||
this.infoModalLink = link;
|
||||
this.infoModalAdditionalButtons = buttons;
|
||||
@@ -74,8 +82,9 @@ export default class ModalStore {
|
||||
}
|
||||
if (this.showInfoModal) {
|
||||
this.showInfoModal = false;
|
||||
this.infoModalText = '';
|
||||
this.infoModalLink = '';
|
||||
this.infoModalTitle = undefined;
|
||||
this.infoModalText = undefined;
|
||||
this.infoModalLink = undefined;
|
||||
this.infoModalAdditionalButtons = undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
+3
-1
@@ -105,7 +105,9 @@ class Stores {
|
||||
this.syncStore = new SyncStore(this.settingsStore);
|
||||
this.cashuStore = new CashuStore(
|
||||
this.settingsStore,
|
||||
this.invoicesStore
|
||||
this.invoicesStore,
|
||||
this.channelsStore,
|
||||
this.modalStore
|
||||
);
|
||||
this.activityStore = new ActivityStore(
|
||||
this.settingsStore,
|
||||
|
||||
@@ -564,14 +564,16 @@ export default class ChannelView extends React.Component<
|
||||
<TouchableOpacity
|
||||
style={{ width: '50%' }}
|
||||
onPress={() => {
|
||||
ModalStore.toggleInfoModal([
|
||||
localeString(
|
||||
'views.Channel.lease.lspDiscretion.explainer1'
|
||||
),
|
||||
localeString(
|
||||
'views.Channel.lease.lspDiscretion.explainer2'
|
||||
)
|
||||
]);
|
||||
ModalStore.toggleInfoModal({
|
||||
text: [
|
||||
localeString(
|
||||
'views.Channel.lease.lspDiscretion.explainer1'
|
||||
),
|
||||
localeString(
|
||||
'views.Channel.lease.lspDiscretion.explainer2'
|
||||
)
|
||||
]
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
|
||||
@@ -159,12 +159,11 @@ export default class Security extends React.Component<
|
||||
const { pin, passphrase } = SettingsStore.settings;
|
||||
|
||||
if (value && !pin && !passphrase) {
|
||||
ModalStore.toggleInfoModal(
|
||||
localeString(
|
||||
ModalStore.toggleInfoModal({
|
||||
text: localeString(
|
||||
'views.Settings.Security.BiometryRequiresPinOrPassword'
|
||||
),
|
||||
undefined,
|
||||
[
|
||||
buttons: [
|
||||
{
|
||||
title: localeString(
|
||||
'views.Settings.createYourPassword'
|
||||
@@ -182,7 +181,7 @@ export default class Security extends React.Component<
|
||||
})
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,15 +214,14 @@ export default class Security extends React.Component<
|
||||
if (!(settings.passphrase || settings.pin)) {
|
||||
navigation.navigate(item.screen);
|
||||
} else if (item.action === 'DeletePin' && isBiometryEnabled) {
|
||||
ModalStore.toggleInfoModal(
|
||||
[
|
||||
ModalStore.toggleInfoModal({
|
||||
text: [
|
||||
localeString(
|
||||
'views.Settings.Security.biometricsWillBeDisabled'
|
||||
),
|
||||
localeString('general.continueQuestion')
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
buttons: [
|
||||
{
|
||||
title: localeString('general.ok'),
|
||||
callback: () =>
|
||||
@@ -232,7 +230,7 @@ export default class Security extends React.Component<
|
||||
})
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
} else if (item.action === 'DeletePin') {
|
||||
navigation.navigate('Lockscreen', {
|
||||
deletePin: true
|
||||
|
||||
@@ -270,12 +270,11 @@ export default class SetPassphrase extends React.Component<
|
||||
confirmDelete: true
|
||||
});
|
||||
} else if (this.state.isBiometryEnabled) {
|
||||
this.props.ModalStore.toggleInfoModal(
|
||||
localeString(
|
||||
this.props.ModalStore.toggleInfoModal({
|
||||
text: localeString(
|
||||
'views.Settings.Security.biometricsWillBeDisabled'
|
||||
),
|
||||
undefined,
|
||||
[
|
||||
buttons: [
|
||||
{
|
||||
title: localeString(
|
||||
'general.ok'
|
||||
@@ -284,7 +283,7 @@ export default class SetPassphrase extends React.Component<
|
||||
this.deletePassword()
|
||||
}
|
||||
]
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.deletePassword();
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
if (BackendUtils.supportsAccounts()) UTXOsStore.listAccounts();
|
||||
await BalanceStore.getCombinedBalance(false);
|
||||
if (BackendUtils.supportsChannelManagement())
|
||||
ChannelsStore.getChannels();
|
||||
await ChannelsStore.getChannels();
|
||||
if (rescan) {
|
||||
await updateSettings({
|
||||
rescan: false
|
||||
@@ -603,6 +603,14 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
LSPStore.initChannelAcceptor();
|
||||
}
|
||||
|
||||
// Check Cashu balance for upgrade prompts
|
||||
if (implementation === 'embedded-lnd' && settings?.ecash?.enableCashu) {
|
||||
CashuStore.checkAndShowUpgradeModal(
|
||||
0,
|
||||
CashuStore.totalBalanceSats || 0
|
||||
);
|
||||
}
|
||||
|
||||
// only navigate to initial url after connection and main calls are made
|
||||
if (
|
||||
this.state.initialLoad &&
|
||||
|
||||
Reference in New Issue
Block a user