Skip to content

Commit 221c0a7

Browse files
NYM-589: privy linking fixes (#4718)
1 parent 9ba30bb commit 221c0a7

10 files changed

Lines changed: 181 additions & 29 deletions

File tree

nym-vpn-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
### Fixed
1515

1616
- Improve Accessibility for sliders
17+
- Fix privy social linking check
1718

1819
## [1.24.0] - 2026-02-17
1920

nym-vpn-app/src-tauri/src/commands/account.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use tauri::State;
22
use tracing::{error, info, instrument, warn};
33

44
use crate::state::SharedAppState;
5-
use crate::vpnd::account::AccountState;
65
use crate::vpnd::account::StoredAccountMode;
6+
use crate::vpnd::account::{AccountState, AccountSummary};
77
use crate::vpnd::account_links::AccountLinks;
88
use crate::vpnd::tunnel::TunnelState;
99
use crate::{error::BackendError, vpnd::client::VpndClient};
@@ -166,3 +166,14 @@ pub async fn get_account_mode(
166166
e.into()
167167
})
168168
}
169+
170+
#[instrument(skip_all)]
171+
#[tauri::command]
172+
pub async fn get_account_summary(
173+
vpnd: State<'_, VpndClient>,
174+
) -> Result<Option<AccountSummary>, BackendError> {
175+
vpnd.get_account_summary().await.map_err(|e| {
176+
error!("failed to get account summary: {e}");
177+
e.into()
178+
})
179+
}

nym-vpn-app/src-tauri/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ async fn main() -> Result<()> {
301301
account::account_links,
302302
account::get_deep_link,
303303
account::store_deeplink_account,
304+
account::get_account_summary,
304305
cmd_daemon::daemon_status,
305306
cmd_daemon::set_network,
306307
cmd_daemon::system_messages,

nym-vpn-app/src-tauri/src/vpnd/account.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,76 @@ impl From<lib::StoredAccountMode> for StoredAccountMode {
9292
}
9393
}
9494
}
95+
96+
#[derive(Serialize, Clone, Debug, PartialEq, TS)]
97+
#[ts(export, export_to = "tauri.ts", rename = "TAccountSummary")]
98+
#[serde(rename_all = "kebab-case")]
99+
pub struct AccountSummary {
100+
pub subscription_valid_until: Option<String>,
101+
pub traffic_used_gb: u64,
102+
pub traffic_limit_gb: u64,
103+
pub traffic_reset_time: Option<String>,
104+
pub account_addr: String,
105+
pub canonical_account_addr: Option<String>,
106+
pub auth_methods: Vec<AuthMethod>,
107+
}
108+
109+
impl From<lib::VpnAccountSummary> for AccountSummary {
110+
fn from(summary: lib::VpnAccountSummary) -> Self {
111+
AccountSummary {
112+
subscription_valid_until: summary.subscription_valid_until.map(|dt| dt.to_string()),
113+
traffic_used_gb: summary.traffic_used_gb,
114+
traffic_limit_gb: summary.traffic_limit_gb,
115+
traffic_reset_time: summary.traffic_reset_time.map(|dt| dt.to_string()),
116+
account_addr: summary.account_addr,
117+
canonical_account_addr: summary.canonical_account_addr,
118+
auth_methods: summary
119+
.auth_methods
120+
.into_iter()
121+
.map(AuthMethod::from)
122+
.collect(),
123+
}
124+
}
125+
}
126+
127+
#[derive(Serialize, Clone, Debug, PartialEq, TS)]
128+
#[ts(export, export_to = "tauri.ts", rename = "TAuthMethod")]
129+
#[serde(rename_all = "kebab-case")]
130+
pub struct AuthMethod {
131+
pub id: String,
132+
pub pubkey: String,
133+
pub kind: String,
134+
pub label: String,
135+
pub status: VpnAccountStatus,
136+
}
137+
138+
impl From<lib::VpnAccountAuthMethod> for AuthMethod {
139+
fn from(auth_method: lib::VpnAccountAuthMethod) -> Self {
140+
AuthMethod {
141+
id: auth_method.id,
142+
pubkey: auth_method.pubkey,
143+
kind: auth_method.kind,
144+
label: auth_method.label,
145+
status: auth_method.status.into(),
146+
}
147+
}
148+
}
149+
150+
#[derive(Serialize, Clone, Debug, PartialEq, TS)]
151+
#[ts(export, export_to = "tauri.ts", rename = "TVpnAccountStatus")]
152+
#[serde(rename_all = "kebab-case")]
153+
pub enum VpnAccountStatus {
154+
Active,
155+
Inactive,
156+
DeleteMe,
157+
}
158+
159+
impl From<lib::VpnAccountStatus> for VpnAccountStatus {
160+
fn from(status: lib::VpnAccountStatus) -> Self {
161+
match status {
162+
lib::VpnAccountStatus::Active => VpnAccountStatus::Active,
163+
lib::VpnAccountStatus::Inactive => VpnAccountStatus::Inactive,
164+
lib::VpnAccountStatus::DeleteMe => VpnAccountStatus::DeleteMe,
165+
}
166+
}
167+
}

nym-vpn-app/src-tauri/src/vpnd/client.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub use super::{
2-
account::StoredAccountMode,
2+
account::{AccountSummary, StoredAccountMode},
33
account_links::AccountLinks,
44
error::VpndError,
55
feature_flags::FeatureFlags,
@@ -908,4 +908,15 @@ impl VpndClient {
908908
.or_else(async |e| self.handle_rpc_error("set_mixnet_traffic_config", e).await)
909909
.await
910910
}
911+
912+
pub async fn get_account_summary(&self) -> Result<Option<AccountSummary>, VpndError> {
913+
let mut vpnd = self.vpnd().await?;
914+
915+
let summary = vpnd
916+
.get_account_summary()
917+
.or_else(async |e| self.handle_rpc_error("get_account_summary", e).await)
918+
.await?;
919+
920+
Ok(summary.map(Into::into))
921+
}
911922
}

nym-vpn-app/src/contexts/main/reducer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
ProgressMsg,
2222
SelectedNode,
2323
TAccountMode,
24+
TAccountSummary,
2425
ThemeMode,
2526
Tunnel,
2627
TunnelAction,
@@ -85,7 +86,8 @@ export type StateAction =
8586
| { type: 'set-custom-dns'; dns: string[] }
8687
| { type: 'set-default-dns'; dns: string[] }
8788
| { type: 'set-enable-lewes-protocol'; enabled: boolean }
88-
| { type: 'set-mixnet-traffic-config'; config: MixnetTrafficConfig };
89+
| { type: 'set-mixnet-traffic-config'; config: MixnetTrafficConfig }
90+
| { type: 'set-account-summary'; summary: TAccountSummary };
8991

9092
export const initialState: AppState = {
9193
initialized: false,
@@ -94,6 +96,7 @@ export const initialState: AppState = {
9496
tunnelError: null,
9597
accountState: null,
9698
accountMode: null,
99+
accountSummary: null,
97100
accountSyncing: false,
98101
accountError: null,
99102
daemonStatus: 'down',
@@ -457,6 +460,11 @@ export function reducer(state: AppState, action: StateAction): AppState {
457460
...state,
458461
mixnetTrafficConfig: action.config,
459462
};
463+
case 'set-account-summary':
464+
return {
465+
...state,
466+
accountSummary: action.summary,
467+
};
460468
case 'reset':
461469
return initialState;
462470
}

nym-vpn-app/src/screens/settings/account/Account.tsx

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTranslation } from 'react-i18next';
2-
import { useCallback, useEffect, useState } from 'react';
2+
import { useEffect, useState } from 'react';
33
import { invoke } from '@tauri-apps/api/core';
44
import { openUrl } from '@tauri-apps/plugin-opener';
55
import { useNavigate } from 'react-router';
@@ -23,7 +23,7 @@ import {
2323
} from '../../../contexts';
2424
import { routes } from '../../../router';
2525
import { useDeepLink, useLogout } from '../../../hooks';
26-
import { StateDispatch, TAccountMode } from '../../../types';
26+
import { StateDispatch, TAccountMode, TAccountSummary } from '../../../types';
2727
import { getAccountColor, getAccountDescription } from './utils';
2828

2929
const IdsTimeToLive = 120; // sec
@@ -40,6 +40,7 @@ function Account() {
4040
accountSyncing,
4141
daemonStatus,
4242
accountMode,
43+
accountSummary,
4344
backendFlags,
4445
} = useMainState();
4546
const dispatch = useMainDispatch() as StateDispatch;
@@ -49,29 +50,43 @@ function Account() {
4950
accountState === 'bandwidth-exceeded');
5051

5152
const [isAccountLinking, setIsAccountLinking] = useState(false);
52-
const [accountId, setAccountId] = useState<string | null>(null);
5353
const [deviceId, setDeviceId] = useState<string | null>(null);
5454

55-
const linkable = accountMode === 'api';
55+
// Privy and linking logic
56+
const isLoggedWithPrivy = accountMode === 'privy';
57+
const isDifferentCanonical =
58+
accountSummary?.['account-addr'] !==
59+
accountSummary?.['canonical-account-addr'];
60+
const hasLinkedAuthMethod = accountSummary?.['auth-methods']?.some(
61+
(it) => it.label === 'Social login' || it.label === 'Passphrase',
62+
);
63+
64+
const isAccountLinked =
65+
isLoggedWithPrivy || isDifferentCanonical || hasLinkedAuthMethod;
5666

5767
const { startListening } = useDeepLink();
5868
const { push } = useInAppNotify();
5969

60-
const getAccountId = async () => {
61-
const accountId = await CCache.get<string>('cache-account-id');
62-
if (accountId) {
63-
setAccountId(accountId);
64-
return;
70+
const refreshAccount = async () => {
71+
try {
72+
const summary = await invoke<TAccountSummary>('get_account_summary');
73+
dispatch({ type: 'set-account-summary', summary });
74+
} catch (err) {
75+
console.error('Failed to get account summary', err);
6576
}
6677
try {
67-
const accountId = await invoke<string>('get_account_id');
68-
setAccountId(accountId);
69-
CCache.set('cache-account-id', accountId, IdsTimeToLive);
70-
} catch {
71-
setAccountId(null);
78+
const mode = await invoke<TAccountMode>('get_account_mode');
79+
dispatch({ type: 'set-account-mode', mode });
80+
} catch (err) {
81+
console.error('Failed to get account mode', err);
7282
}
7383
};
7484

85+
useEffect(() => {
86+
refreshAccount();
87+
// eslint-disable-next-line react-hooks/exhaustive-deps
88+
}, []);
89+
7590
const getDeviceId = async () => {
7691
const deviceId = await CCache.get<string>('cache-device-id');
7792
if (deviceId) {
@@ -88,19 +103,14 @@ function Account() {
88103
};
89104

90105
useEffect(() => {
91-
getAccountId();
92106
getDeviceId();
93107
}, []);
94108

109+
// When logged out, navigate to settings
95110
useEffect(() => {
96111
if (!account) navigate(routes.settings, { replace: true });
97112
}, [account, navigate]);
98113

99-
const refreshAccountMode = useCallback(async () => {
100-
const mode = await invoke<TAccountMode>('get_account_mode');
101-
dispatch({ type: 'set-account-mode', mode });
102-
}, [dispatch]);
103-
104114
const handleAccountLink = async () => {
105115
setIsAccountLinking(true);
106116

@@ -121,7 +131,7 @@ function Account() {
121131
await invoke('store_deeplink_account', {
122132
callbackUrl: deeplinkUrl,
123133
});
124-
await refreshAccountMode();
134+
await refreshAccount();
125135
} catch (error) {
126136
console.error('Account login error: ', error);
127137
if (error instanceof Error && error.message === 'Login timeout') {
@@ -177,7 +187,7 @@ function Account() {
177187
trailingIcon: 'open_in_new',
178188
onClick: handleManageSubscription,
179189
},
180-
...(backendFlags.privy && linkable
190+
...(backendFlags.privy && isAccountLinked
181191
? [
182192
{
183193
title: t('account.account-on-nym'),
@@ -194,7 +204,7 @@ function Account() {
194204

195205
{backendFlags.privy && (
196206
<p className="text-sm text-iron dark:text-bombay">
197-
{linkable
207+
{isAccountLinked
198208
? t('account.account-not-linked')
199209
: t('account.account-linked')}
200210
</p>
@@ -211,9 +221,10 @@ function Account() {
211221
</CardNewHeader>
212222
<CardNewBody className="pb-5">
213223
<CardNewCopyableRow
214-
value={accountId ?? ''}
215-
label={accountId ?? ''}
216-
loading={!accountId}
224+
// Displaying canonical account address, as this is NYM's default account address
225+
value={accountSummary?.['canonical-account-addr'] ?? ''}
226+
label={accountSummary?.['canonical-account-addr'] ?? ''}
227+
loading={!accountSummary?.['canonical-account-addr']}
217228
/>
218229
</CardNewBody>
219230
</CardNew>

nym-vpn-app/src/state/init.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
StateDispatch,
1616
TAccountMode,
1717
TAccountState,
18+
TAccountSummary,
1819
TTunnelState,
1920
ThemeMode,
2021
UiTheme,
@@ -81,6 +82,18 @@ export async function initFirstBatch(
8182
},
8283
};
8384

85+
const getAccountSummaryRq: TauriReq<
86+
() => Promise<TAccountSummary | undefined>
87+
> = {
88+
name: 'getAccountSummaryRq',
89+
request: () => invoke<TAccountSummary>('get_account_summary'),
90+
onFulfilled: (summary) => {
91+
if (summary) {
92+
dispatch({ type: 'set-account-summary', summary });
93+
}
94+
},
95+
};
96+
8497
const getFeatureFlagsRq: TauriReq<() => Promise<FeatureFlags | undefined>> = {
8598
name: 'getFeatureFlagsRq',
8699
request: () => invoke<FeatureFlags>('feature_flags'),
@@ -214,6 +227,7 @@ export async function initFirstBatch(
214227
getStoredAccountRq,
215228
getAccountStateRq,
216229
getAccountModeRq,
230+
getAccountSummaryRq,
217231
getFeatureFlagsRq,
218232
...requests,
219233
];

nym-vpn-app/src/types/app-state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
NetworkEnv,
1212
SelectedNode,
1313
TAccountMode,
14+
TAccountSummary,
1415
ThemeMode,
1516
Tunnel,
1617
TunnelError,
@@ -57,6 +58,7 @@ export type AppState = {
5758
tunnelError?: TunnelError | null;
5859
accountState?: AccountState | null;
5960
accountMode?: TAccountMode | null;
61+
accountSummary?: TAccountSummary | null;
6062
accountError?: AppError | null;
6163
accountSyncing: boolean;
6264
daemonStatus: DaemonStatus;

0 commit comments

Comments
 (0)