refactor(nwc): polish connection QR screen layout and status UI

This commit is contained in:
ajaysehwal
2026-06-04 10:52:41 -04:00
committed by Evan Kaloudis
parent aa5b8fd314
commit 1464d45997
6 changed files with 205 additions and 116 deletions
+1 -3
View File
@@ -173,9 +173,7 @@ export default class FeeEstimate extends React.PureComponent<
{ color: themeColor('secondaryText') }
]}
>
{localeString(
'components.NWCPendingPayInvoiceModal.totalAmount'
)}
{localeString('general.totalAmount')}
</Text>
</View>
</View>
+4 -3
View File
@@ -66,6 +66,7 @@
"general.frozen": "Frozen",
"general.optional": "Optional",
"general.sats": "sats",
"general.totalAmount": "Total Amount",
"general.blocks": "blocks",
"general.search": "Search",
"general.help": "Help",
@@ -1985,11 +1986,13 @@
"views.Settings.NostrWalletConnect.connectionId": "Connection ID",
"views.Settings.NostrWalletConnect.publicKey": "Public Key",
"views.Settings.NostrWalletConnect.connectionSecret": "Connection Secret",
"views.Settings.NostrWalletConnect.connectionSecretDescription": "Scan this QR code or copy the connection secret in your Nostr app to link this connection.",
"views.Settings.NostrWalletConnect.waitingForAppToConnect": "Waiting for app to connect...",
"views.Settings.NostrWalletConnect.noEventReceivedTimeoutDescription": "We didn't receive any event from your Nostr client, but you can still close this screen and use your connection. The connection will work once your Nostr client sends a request.",
"views.Settings.NostrWalletConnect.connectionTimeout": "Connection Timeout",
"views.Settings.NostrWalletConnect.keepWaiting": "Keep Waiting",
"views.Settings.NostrWalletConnect.connectedSuccessfully": "Connected Successfully!",
"views.Settings.NostrWalletConnect.connectedSuccessfully": "Connected successfully!",
"views.Settings.NostrWalletConnect.returningToConnections": "Returning to connections…",
"views.Settings.NostrWalletConnect.noConnectionsFound": "No connections found",
"views.Settings.NostrWalletConnect.leftInBudget": "Left in budget",
"views.Settings.NostrWalletConnect.switchToCashuWallet": "Switch to Cashu wallet",
@@ -2029,7 +2032,6 @@
"views.Settings.NostrWalletConnect.error.failedToUpdateNwcSetting": "Failed to update NWC setting",
"views.Settings.NostrWalletConnect.error.invalidAmount": "Invalid amount.",
"views.Settings.NostrWalletConnect.error.paymentExceedsBudget": "This payment ({{amount}} sats) exceeds the remaining budget for this connection ({{remaining}} sats).",
"views.Settings.NostrWalletConnect.confirmDeletePendingPayments": "Are you sure you want to delete all pending payments?",
"views.Settings.NostrWalletConnect.totalSpent": "Total Spent",
"views.Settings.NostrWalletConnect.totalReceived": "Total Received",
"views.Settings.NostrWalletConnect.remainingBudget": "Remaining Budget",
@@ -2037,7 +2039,6 @@
"views.Settings.NostrWalletConnect.warning.budgetLimitReached": "The assigned budget for this connection has reached its limit. Please review and update it if you wish to continue using this connection.",
"views.Settings.NostrWalletConnect.nwcSettings": "NWC Settings",
"views.Settings.NostrWalletConnect.persistentNWCService": "Persistent Mode",
"views.Settings.NostrWalletConnect.persistentNWCServiceDescription": "Continue to run Nostr Wallet Connect service in the background even when the app is closed.",
"views.Settings.NostrWalletConnect.nwcIncludeLud16InUrl": "Include Lightning Address",
"views.Settings.NostrWalletConnect.nwcIncludeLud16InUrlDescription": "When enabled, your Lightning address (LUD-16) is added to new and regenerated NWC connection URLs. Some Nostr clients will set this as your Lightning address in your profile. Turn off if you prefer not to share it with the connecting app.",
"views.Settings.NostrWalletConnect.persistentNWCServiceDescriptionAndroid": "Keeps Nostr Wallet Connect active while Zeus is in the background. Android shows a persistent foreground notification so the service is not stopped. Restart Zeus after changing this setting.",
-1
View File
@@ -15,7 +15,6 @@ export default class ModalStore {
@observable public showAlertModal: boolean = false;
@observable public showShareModal: boolean = false;
@observable public showNewChannelModal: boolean = false;
@observable public showNWCPendingPaymentsModal: boolean = false;
@observable public showRatingModal: boolean = false;
@observable public showRestoreChannelModal: boolean = false;
@observable public modalUrl: string;
+6
View File
@@ -27,6 +27,12 @@ jest.mock('./BackendUtils', () => ({
default: {}
}));
jest.mock('react-native-notifications', () => ({
Notifications: {
postLocalNotification: jest.fn()
}
}));
import * as nostrTools from 'nostr-tools';
import NostrConnectUtils from './NostrConnectUtils';
@@ -22,8 +22,9 @@ import ModalBox from '../../../components/ModalBox';
import NostrWalletConnectStore from '../../../stores/NostrWalletConnectStore';
import { themeColor } from '../../../utils/ThemeUtils';
import { localeString } from '../../../utils/LocaleUtils';
import AlertIcon from '../../../assets/images/SVG/Alert.svg';
import { font } from '../../../utils/FontUtils';
import AlertIcon from '../../../assets/images/SVG/Alert.svg';
import SuccessAnimation from '../../../components/SuccessAnimation';
interface NWCConnectionQRProps {
navigation: NativeStackNavigationProp<any, any>;
@@ -37,6 +38,8 @@ interface NWCConnectionQRState {
showTimeoutMessage: boolean;
}
const CONNECTION_TIMEOUT_MS = 20000;
const CONNECTED_REDIRECT_MS = 2000;
@inject('NostrWalletConnectStore')
@observer
export default class NWCConnectionQR extends React.Component<
@@ -45,6 +48,7 @@ export default class NWCConnectionQR extends React.Component<
> {
appStateSubscription: NativeEventSubscription | null = null;
connectionTimeout: ReturnType<typeof setTimeout> | null = null;
navigateBackTimeout: ReturnType<typeof setTimeout> | null = null;
constructor(props: NWCConnectionQRProps) {
super(props);
@@ -84,9 +88,9 @@ export default class NWCConnectionQR extends React.Component<
this.connectionTimeout = null;
}
this.setState({ isConnected: true, showTimeoutMessage: false });
setTimeout(() => {
this.navigateBackTimeout = setTimeout(() => {
navigation.popTo('NostrWalletConnect');
}, 2000);
}, CONNECTED_REDIRECT_MS);
}
}
@@ -100,6 +104,10 @@ export default class NWCConnectionQR extends React.Component<
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
if (this.navigateBackTimeout) {
clearTimeout(this.navigateBackTimeout);
this.navigateBackTimeout = null;
}
}
handleAppStateChange = (nextAppState: AppStateStatus) => {
@@ -130,9 +138,80 @@ export default class NWCConnectionQR extends React.Component<
navigation.popTo('NostrWalletConnect');
};
renderConnectedSuccess = () => (
<View style={styles.connectedContainer}>
<View style={styles.connectedAnimationWrap}>
<SuccessAnimation />
</View>
<Text
style={[styles.connectedTitle, { color: themeColor('text') }]}
>
{localeString(
'views.Settings.NostrWalletConnect.connectedSuccessfully'
)}
</Text>
<Text
style={[
styles.connectedSubtitle,
{ color: themeColor('secondaryText') }
]}
>
{localeString(
'views.Settings.NostrWalletConnect.returningToConnections'
)}
</Text>
</View>
);
renderWaitingStatus = () => {
const { NostrWalletConnectStore } = this.props;
if (!NostrWalletConnectStore.waitingForConnection) {
return null;
}
return (
<View style={styles.statusContainer}>
<LoadingIndicator size={36} />
<Text
style={[
styles.statusText,
{ color: themeColor('secondaryText') }
]}
>
{localeString(
'views.Settings.NostrWalletConnect.waitingForAppToConnect'
)}
</Text>
</View>
);
};
renderConnectionContent = (nostrUrl: string) => (
<>
<Text
style={[
styles.description,
{ color: themeColor('secondaryText') }
]}
>
{localeString(
'views.Settings.NostrWalletConnect.connectionSecretDescription'
)}
</Text>
<View style={styles.qrCard}>
<CollapsedQR value={nostrUrl} hideText={true} expanded />
</View>
{this.renderWaitingStatus()}
</>
);
render() {
const { navigation, NostrWalletConnectStore, route } = this.props;
const { nostrUrl } = route.params;
const { isConnected } = this.state;
return (
<Screen>
@@ -150,77 +229,53 @@ export default class NWCConnectionQR extends React.Component<
}
}}
/>
<ScrollView
style={{
paddingHorizontal: 15
}}
>
<CollapsedQR value={nostrUrl} hideText={true} expanded />
<View style={styles.main}>
<ScrollView
style={styles.scroll}
contentContainerStyle={[
styles.scrollContent,
isConnected && styles.scrollContentConnected
]}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{isConnected
? this.renderConnectedSuccess()
: this.renderConnectionContent(nostrUrl)}
</ScrollView>
{NostrWalletConnectStore.waitingForConnection && (
<View style={[styles.loadingContainer]}>
<LoadingIndicator />
<Text
style={[
styles.loadingText,
{ color: themeColor('secondaryText') }
]}
>
{localeString(
'views.Settings.NostrWalletConnect.waitingForAppToConnect'
)}
</Text>
{!isConnected && (
<View
style={[
styles.footer,
{ backgroundColor: themeColor('background') }
]}
>
<Button
title={localeString('general.close')}
onPress={() => {
navigation.popTo('NostrWalletConnect');
}}
secondary
noUppercase
/>
</View>
)}
</View>
{this.state.isConnected && (
<View style={[styles.connectedContainer]}>
<Text
style={[
styles.connectedText,
{ color: themeColor('success') }
]}
>
{localeString(
'views.Settings.NostrWalletConnect.connectedSuccessfully'
)}
</Text>
</View>
)}
<View style={styles.buttonContainer}>
<Button
title={localeString('general.close')}
onPress={() => {
navigation.popTo('NostrWalletConnect');
}}
secondary
noUppercase
/>
</View>
</ScrollView>
<ModalBox
isOpen={
this.state.showTimeoutMessage &&
NostrWalletConnectStore.waitingForConnection
}
style={{
backgroundColor: 'transparent',
minHeight: 200,
zIndex: 9999
}}
style={styles.modalBackdrop}
onClosed={() =>
this.setState({ showTimeoutMessage: false })
}
backdropPressToClose={false}
swipeToClose={false}
>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<View style={styles.modalOverlay}>
<View
style={[
styles.modalContainer,
@@ -283,60 +338,95 @@ export default class NWCConnectionQR extends React.Component<
}
const styles = StyleSheet.create({
loadingContainer: {
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
paddingVertical: 5,
borderRadius: 12,
paddingHorizontal: 10
main: {
flex: 1
},
loadingText: {
fontSize: 16,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
marginTop: 12,
fontWeight: '500'
scroll: {
flex: 1
},
buttonContainer: {
width: '100%',
scrollContent: {
flexGrow: 1,
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 24,
alignItems: 'center'
},
connectedContainer: {
justifyContent: 'center',
alignItems: 'center',
marginBottom: 40,
scrollContentConnected: {
justifyContent: 'center'
},
description: {
fontSize: 15,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
lineHeight: 22,
maxWidth: 340
},
qrCard: {
width: '100%',
maxWidth: 340,
borderRadius: 20,
paddingVertical: 20,
paddingHorizontal: 24
paddingHorizontal: 12,
alignItems: 'center'
},
connectedText: {
fontSize: 16,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
fontWeight: '600'
},
iosTimerContainer: {
statusContainer: {
marginTop: 28,
alignItems: 'center',
marginVertical: 16,
paddingVertical: 16
justifyContent: 'center',
gap: 12,
paddingHorizontal: 16,
maxWidth: 340
},
iosTimerTitle: {
fontSize: 18,
fontFamily: 'PPNeueMontreal-Book',
fontWeight: '600',
textAlign: 'center',
marginBottom: 16
},
iosTimerSubtitle: {
fontSize: 14,
statusText: {
fontSize: 15,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
marginTop: 12,
lineHeight: 20
lineHeight: 22
},
connectedContainer: {
flex: 1,
width: '100%',
maxWidth: 340,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40
},
connectedAnimationWrap: {
alignItems: 'center',
marginBottom: 15
},
connectedTitle: {
fontSize: 28,
fontFamily: font('marlideBold'),
textAlign: 'center',
marginBottom: 8
},
connectedSubtitle: {
fontSize: 15,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
lineHeight: 22
},
footer: {
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 16
},
modalBackdrop: {
backgroundColor: 'transparent',
minHeight: 200,
zIndex: 9999
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
modalContainer: {
borderRadius: 30,
padding: 30,
paddingHorizontal: 28,
paddingTop: 32,
paddingBottom: 28,
width: '90%',
maxWidth: 400,
alignItems: 'center',
@@ -350,23 +440,22 @@ const styles = StyleSheet.create({
elevation: 8
},
modalIconContainer: {
marginBottom: 20,
marginBottom: 16,
alignItems: 'center',
justifyContent: 'center'
},
modalTitle: {
fontSize: 28,
fontSize: 26,
fontFamily: font('marlideBold'),
fontWeight: '600',
textAlign: 'center',
marginBottom: 12
},
modalMessage: {
fontSize: 16,
fontSize: 15,
fontFamily: 'PPNeueMontreal-Book',
textAlign: 'center',
lineHeight: 22,
marginBottom: 30
marginBottom: 28
},
modalButtonsContainer: {
width: '100%',
@@ -426,10 +426,6 @@ export default class NWCConnectionsList extends React.Component<
const { NostrWalletConnectStore, navigation, SettingsStore } =
this.props;
const { connections, loading } = NostrWalletConnectStore;
const { settings } = SettingsStore;
const isNwcSettingsAvailable =
Platform.OS === 'android' ||
(BackendUtils.supportsCashuWallet() && settings.ecash.enableCashu);
const { connectionsLoading, error } = this.state;
const { settings } = SettingsStore;
const isNwcSettingsAvailable =