feat: upgrade to self-custody prompts

This commit is contained in:
Evan Kaloudis
2025-04-18 15:50:23 -04:00
parent 791bb0fabe
commit c38bcdf5d8
13 changed files with 193 additions and 65 deletions
+5 -5
View File
@@ -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%' }}
>
+31 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+10 -8
View File
@@ -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
+8 -10
View File
@@ -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
+4 -5
View File
@@ -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();
}
+9 -1
View File
@@ -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 &&