Skip to content

Commit 400b81a

Browse files
(tauri) Update account summary when account state changes (#5002)
1 parent edb2b08 commit 400b81a

15 files changed

Lines changed: 205 additions & 146 deletions

File tree

nym-vpn-app/src/components/privy/PrivyButton.tsx

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { openUrl } from '@tauri-apps/plugin-opener';
2-
import { useEffect, useRef, useState } from 'react';
2+
import { useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { invoke } from '@tauri-apps/api/core';
55
import { useNavigate } from 'react-router';
@@ -9,6 +9,7 @@ import { useDeepLink } from '../../hooks';
99
import { routes } from '../../router';
1010
import { CCache } from '../../cache';
1111
import { StateDispatch, TAccountMode } from '../../types';
12+
import { DeeplinkTimeout } from '../../errors';
1213

1314
function PrivyButton() {
1415
const { t, i18n } = useTranslation('login');
@@ -20,16 +21,6 @@ function PrivyButton() {
2021
const dispatch = useMainDispatch() as StateDispatch;
2122

2223
const [loading, setLoading] = useState(false);
23-
const timeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24-
25-
useEffect(() => {
26-
return () => {
27-
if (timeoutIdRef.current !== null) {
28-
clearTimeout(timeoutIdRef.current);
29-
timeoutIdRef.current = null;
30-
}
31-
};
32-
}, []);
3324

3425
const refreshAccountMode = async () => {
3526
const accountMode = await invoke<TAccountMode>('get_account_mode');
@@ -46,17 +37,7 @@ function PrivyButton() {
4637
openUrl(loginUrl);
4738

4839
try {
49-
const timeoutPromise = new Promise<never>((_, reject) => {
50-
timeoutIdRef.current = setTimeout(
51-
() => reject(new Error('Login timeout')),
52-
300000,
53-
);
54-
});
55-
56-
const deeplinkurl = await Promise.race([
57-
startListening(),
58-
timeoutPromise,
59-
]);
40+
const deeplinkurl = await startListening(300000);
6041

6142
await invoke('store_deeplink_account', {
6243
callbackUrl: deeplinkurl,
@@ -75,7 +56,7 @@ function PrivyButton() {
7556
dispatch({ type: 'reset-error' });
7657
} catch (error) {
7758
console.error('Privy login error: ', error);
78-
if (error instanceof Error && error.message === 'Login timeout') {
59+
if (error instanceof DeeplinkTimeout) {
7960
push({
8061
message: t('privy.error.timeout'),
8162
type: 'error',
@@ -91,10 +72,6 @@ function PrivyButton() {
9172
});
9273
}
9374
} finally {
94-
if (timeoutIdRef.current !== null) {
95-
clearTimeout(timeoutIdRef.current);
96-
timeoutIdRef.current = null;
97-
}
9875
setLoading(false);
9976
}
10077
};

nym-vpn-app/src/contexts/autologin/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export type AutologinKind = Extract<
88

99
export type AutologinContextType = {
1010
autologin: (kind: AutologinKind) => Promise<void>;
11+
closeDialog: () => void;
1112
};
1213

1314
const initialState: AutologinContextType = {
1415
autologin: async () => Promise.resolve(),
16+
closeDialog: () => {
17+
/* SCARECROW */
18+
},
1519
};
1620

1721
export const AutologinContext =

nym-vpn-app/src/contexts/autologin/provider.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,23 @@ export function AutologinProvider({ children }: { children: React.ReactNode }) {
3232
} catch (error) {
3333
console.error('Failed to get autologin deeplink', error);
3434
push({
35-
message: t('autologin.initialization-error'),
35+
message: t('autologin.initialization-error', { ns: 'errors' }),
3636
type: 'error',
3737
duration: 3000,
3838
});
3939
}
4040
},
41-
[i18n.language, t, push],
41+
[i18n.language, push, t],
4242
);
4343

44-
const ctx = useMemo(() => ({ autologin }), [autologin]);
44+
const closeDialog = useCallback(() => {
45+
setOpen(false);
46+
}, []);
47+
48+
const ctx = useMemo(
49+
() => ({ autologin, closeDialog }),
50+
[autologin, closeDialog],
51+
);
4552

4653
return (
4754
<AutologinContext.Provider value={ctx}>

nym-vpn-app/src/contexts/main/provider.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useEffect, useReducer } from 'react';
33
import { InitState, SystemMessage } from '../../types';
44
import { initFirstBatch, initSecondBatch } from '../../state/init';
55
import { useTauriEvents } from '../../state/useTauriEvents';
6+
import { useAccountSummaryOnAccountState } from '../../state/useAccountSummaryOnAccountState';
67
import { useInAppNotify } from '../in-app-notification';
78
import { daemonStatusUpdate, networkEnvChanged } from '../../state/helper';
89
import { CCache } from '../../cache';
@@ -38,6 +39,12 @@ function MainStateProvider({ children, init }: Props) {
3839

3940
const { push } = useInAppNotify();
4041
useTauriEvents(dispatch, push);
42+
useAccountSummaryOnAccountState(
43+
state.accountState,
44+
state.accountSyncing,
45+
state.initialized,
46+
dispatch,
47+
);
4148

4249
// initialize app state
4350
useEffect(() => {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** Thrown when `useDeepLink`’s `startListening` exceeds the given timeout. */
2+
export class DeeplinkTimeout extends Error {
3+
constructor(message = 'Deeplink timed out') {
4+
super(message);
5+
this.name = 'DeeplinkTimeout';
6+
Object.setPrototypeOf(this, new.target.prototype);
7+
}
8+
}

nym-vpn-app/src/errors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { DeeplinkTimeout } from './DeeplinkTimeout';

nym-vpn-app/src/hooks/useDeepLink.ts

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useEffect, useRef } from 'react';
22
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
3+
import { DeeplinkTimeout } from '../errors';
34

45
const useDeepLink = () => {
56
const unlistenRef = useRef<(() => void) | null>(null);
@@ -17,34 +18,59 @@ const useDeepLink = () => {
1718
return cleanup;
1819
}, [cleanup]);
1920

20-
const startListening = useCallback((): Promise<string> => {
21-
isCleanedUpRef.current = false;
22-
unlistenRef.current = null;
23-
24-
return new Promise<string>((resolve, reject) => {
25-
onOpenUrl((urls) => {
26-
if (isCleanedUpRef.current) return;
27-
if (!urls || urls.length === 0) return;
28-
const url = urls[0];
29-
30-
cleanup();
31-
resolve(url);
32-
})
33-
.then((unlistenFn) => {
34-
if (isCleanedUpRef.current) {
35-
unlistenFn();
36-
return;
37-
}
38-
unlistenRef.current = unlistenFn;
21+
const startListening = useCallback(
22+
(timeoutMs?: number): Promise<string> => {
23+
isCleanedUpRef.current = false;
24+
unlistenRef.current = null;
25+
26+
const basePromise = new Promise<string>((resolve, reject) => {
27+
onOpenUrl((urls) => {
28+
if (isCleanedUpRef.current) return;
29+
if (!urls || urls.length === 0) return;
30+
const url = urls[0];
31+
32+
cleanup();
33+
resolve(url);
3934
})
40-
.catch((error: unknown) => {
35+
.then((unlistenFn) => {
36+
if (isCleanedUpRef.current) {
37+
unlistenFn();
38+
return;
39+
}
40+
unlistenRef.current = unlistenFn;
41+
})
42+
.catch((error: unknown) => {
43+
if (!isCleanedUpRef.current) {
44+
cleanup();
45+
reject(error instanceof Error ? error : new Error(String(error)));
46+
}
47+
});
48+
});
49+
50+
if (timeoutMs === undefined) {
51+
return basePromise;
52+
}
53+
54+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
55+
const timeoutPromise = new Promise<never>((_, reject) => {
56+
timeoutId = setTimeout(() => {
57+
cleanup();
4158
if (!isCleanedUpRef.current) {
42-
cleanup();
43-
reject(error instanceof Error ? error : new Error(String(error)));
59+
isCleanedUpRef.current = true;
4460
}
45-
});
46-
});
47-
}, [cleanup]);
61+
reject(new DeeplinkTimeout());
62+
}, timeoutMs);
63+
});
64+
65+
return Promise.race([basePromise, timeoutPromise]).finally(() => {
66+
if (timeoutId !== null) {
67+
clearTimeout(timeoutId);
68+
timeoutId = null;
69+
}
70+
});
71+
},
72+
[cleanup],
73+
);
4874

4975
return { startListening };
5076
};

nym-vpn-app/src/i18n/en/account.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
"autologin": {
2121
"title": "Pin code",
2222
"description": "Enter this code in web",
23-
"copy-code": "Copy code and open web",
24-
"initialization-error": "Failed to initiate autologin"
23+
"copy-code": "Copy code and open web"
2524
}
2625
}

nym-vpn-app/src/i18n/en/errors.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
"device-time-out-of-sync": "Device time is out of sync. Please synchronize your device's time with the internet.",
4848
"offline": "Device is offline"
4949
},
50+
"autologin": {
51+
"timeout": "Autologin timeout",
52+
"initialization-error": "Autologin initialization error"
53+
},
5054
"countries-request": {
5155
"entry": "Failed to fetch the available entry node countries",
5256
"exit": "Failed to fetch the available exit node countries",

nym-vpn-app/src/screens/account/SelectPlan.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { useTranslation } from 'react-i18next';
22
import { useNavigate } from 'react-router';
33
import { useState } from 'react';
4+
import { invoke } from '@tauri-apps/api/core';
45
import { Button, MsIcon, PageAnim, Spinner } from '../../ui';
56
import { routes } from '../../router';
67
import { CheckCircleIcon } from '../../assets';
78
import { useAutologin } from '../../contexts/autologin/context';
9+
import { useDeepLink } from '../../hooks';
10+
import { DeeplinkTimeout } from '../../errors';
11+
import { useInAppNotify } from '../../contexts';
812

913
function Feature({ icon, title }: { icon: string; title: string }) {
1014
return (
@@ -19,15 +23,31 @@ function SelectPlan() {
1923
const { t } = useTranslation('account');
2024

2125
const [autologinLoading, setAutologinLoading] = useState(false);
22-
const { autologin } = useAutologin();
26+
const { autologin, closeDialog } = useAutologin();
27+
const { startListening } = useDeepLink();
28+
const { push } = useInAppNotify();
2329

2430
const handleClick = async () => {
2531
if (autologinLoading) return;
2632

2733
setAutologinLoading(true);
2834
try {
2935
await autologin('autologinRenew');
36+
37+
await startListening(600000);
38+
39+
await invoke<void>('handle_subscription_payment');
40+
closeDialog();
3041
navigate(routes.root);
42+
} catch (error: unknown) {
43+
console.error('Select plan error: ', error);
44+
if (error instanceof DeeplinkTimeout) {
45+
push({
46+
message: t('autologin.timeout', { ns: 'errors' }),
47+
type: 'error',
48+
duration: 3000,
49+
});
50+
}
3151
} finally {
3252
setAutologinLoading(false);
3353
}

0 commit comments

Comments
 (0)