Skip to content

Commit 8a1376f

Browse files
committed
Add dedicated Auto-Lock Timer settings page under Security
Move the auto-lock timer from a dropdown in Preferences to a dedicated full-page selector under Security, matching the Figma design at node 5578:17977. Changes: - @shared/constants/autoLock.ts: expand VALID_AUTO_LOCK_TIMEOUT_MINUTES to [1, 5, 15, 30, 60, 240, 1440] (adds 4h and 24h options); move formatTimeoutLabel helper here from Preferences - Security/index.tsx: add Auto-lock timer nav item with Icon.ClockSnooze - AutoLockTimer/index.tsx: new view with tap-to-select list; reads live selected value from Redux (autoLockTimeoutMinutesSelector), saves immediately on tap with isSaving guard - AutoLockTimer/styles.scss: card-style option rows with dividers and check icon - Preferences/index.tsx: remove auto-lock timer section; pass through current Redux value unchanged on form submit - routes.ts, metricsNames.ts, metrics/views.ts: register new route - Router.tsx: add route for AutoLockTimer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c9487e6 commit 8a1376f

9 files changed

Lines changed: 242 additions & 76 deletions

File tree

@shared/constants/autoLock.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* signing windows, grant-access windows). Any user interaction inside
77
* an extension page resets the timer.
88
*/
9-
export const VALID_AUTO_LOCK_TIMEOUT_MINUTES = [1, 5, 15, 30, 60] as const;
9+
export const VALID_AUTO_LOCK_TIMEOUT_MINUTES = [
10+
1, 5, 15, 30, 60, 240, 1440,
11+
] as const;
1012

1113
export type AutoLockTimeoutMinutes =
1214
(typeof VALID_AUTO_LOCK_TIMEOUT_MINUTES)[number];
@@ -25,3 +27,29 @@ export const coerceAutoLockTimeoutMinutes = (
2527
isValidAutoLockTimeoutMinutes(value)
2628
? value
2729
: DEFAULT_AUTO_LOCK_TIMEOUT_MINUTES;
30+
31+
/**
32+
* Build a human-readable label for a timeout preset. The English fallback is
33+
* constructed in JS and passed to `t()` as `defaultValue`, so even when an
34+
* i18n key is missing (common in dev or partial locales) the rendered label is
35+
* grammatically correct (e.g. "1 minute" / "5 minutes"). Locales can override
36+
* by providing the matching keys.
37+
*/
38+
export const formatTimeoutLabel = (
39+
minutes: AutoLockTimeoutMinutes,
40+
t: (key: string, opts?: Record<string, unknown>) => string,
41+
): string => {
42+
if (minutes >= 60) {
43+
const hours = minutes / 60;
44+
const fallback = hours === 1 ? "1 hour" : `${hours} hours`;
45+
return t("autoLockTimeout.hours", {
46+
count: hours,
47+
defaultValue: fallback,
48+
});
49+
}
50+
const fallback = minutes === 1 ? "1 minute" : `${minutes} minutes`;
51+
return t("autoLockTimeout.minutes", {
52+
count: minutes,
53+
defaultValue: fallback,
54+
});
55+
};

extension/src/popup/Router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { Settings } from "popup/views/Settings";
5050
import { Preferences } from "popup/views/Preferences";
5151
import { Security } from "popup/views/Security";
5252
import { AdvancedSettings } from "popup/views/AdvancedSettings";
53+
import { AutoLockTimer } from "popup/views/AutoLockTimer";
5354
import { About } from "popup/views/About";
5455
import { Send } from "popup/views/Send";
5556
import { ManageAssets } from "popup/views/ManageAssets";
@@ -294,6 +295,10 @@ export const Router = () => (
294295
path={ROUTES.advancedSettings}
295296
element={<AdvancedSettings />}
296297
></Route>
298+
<Route
299+
path={ROUTES.autoLockTimer}
300+
element={<AutoLockTimer />}
301+
></Route>
297302
<Route path={ROUTES.addFunds} element={<AddFunds />} />
298303
<Route path={ROUTES.wallets} element={<Wallets />} />
299304

extension/src/popup/constants/metricsNames.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const METRIC_NAMES = {
3434
viewAbout: "loaded screen: about",
3535
viewManageAssetsLists: "loaded screen: manage assets lists",
3636
viewAdvancedSettings: "loaded screen: advanced settings",
37+
viewAutoLockTimer: "loaded screen: auto-lock timer",
3738

3839
viewSendPayment: "loaded screen: send payment",
3940
sendPaymentTo: "loaded screen: send payment to",

extension/src/popup/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export enum ROUTES {
3636
manageAssetsLists = "/settings/manage-assets-lists",
3737
manageAssetsListsModifyAssetList = "/settings/manage-assets-lists/modify-asset-list",
3838
advancedSettings = "/settings/advanced-settings",
39+
autoLockTimer = "/settings/security/auto-lock-timer",
3940
addFunds = "/add-funds",
4041

4142
addCollectibles = "/add-collectibles",

extension/src/popup/metrics/views.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const routeToEventName = {
6666
[ROUTES.accountMigrationMigrationComplete]:
6767
METRIC_NAMES.viewAccountMigrationMigrationComplete,
6868
[ROUTES.advancedSettings]: METRIC_NAMES.viewAdvancedSettings,
69+
[ROUTES.autoLockTimer]: METRIC_NAMES.viewAutoLockTimer,
6970
[ROUTES.addFunds]: METRIC_NAMES.viewAddFunds,
7071
[ROUTES.wallets]: METRIC_NAMES.wallets,
7172
[ROUTES.confirmSidebarRequest]: METRIC_NAMES.confirmSidebarRequest,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, { useEffect, useState } from "react";
2+
import { useTranslation } from "react-i18next";
3+
import { useDispatch, useSelector } from "react-redux";
4+
import { Navigate, useLocation } from "react-router-dom";
5+
import { Icon, Notification } from "@stellar/design-system";
6+
7+
import {
8+
AutoLockTimeoutMinutes,
9+
VALID_AUTO_LOCK_TIMEOUT_MINUTES,
10+
coerceAutoLockTimeoutMinutes,
11+
formatTimeoutLabel,
12+
} from "@shared/constants/autoLock";
13+
import { AppDispatch } from "popup/App";
14+
import { SubviewHeader } from "popup/components/SubviewHeader";
15+
import { View } from "popup/basics/layout/View";
16+
import { Loading } from "popup/components/Loading";
17+
import { AppDataType, useGetAppData } from "helpers/hooks/useGetAppData";
18+
import { RequestState } from "constants/request";
19+
import { openTab } from "popup/helpers/navigate";
20+
import { newTabHref } from "helpers/urls";
21+
import { reRouteOnboarding } from "popup/helpers/route";
22+
import {
23+
autoLockTimeoutMinutesSelector,
24+
saveSettings,
25+
settingsSelector,
26+
} from "popup/ducks/settings";
27+
28+
import "./styles.scss";
29+
30+
export const AutoLockTimer = () => {
31+
const { t } = useTranslation();
32+
const location = useLocation();
33+
const dispatch = useDispatch<AppDispatch>();
34+
const { state, fetchData } = useGetAppData();
35+
const currentTimeout = useSelector(autoLockTimeoutMinutesSelector);
36+
const settings = useSelector(settingsSelector);
37+
38+
const [isSaving, setIsSaving] = useState(false);
39+
40+
useEffect(() => {
41+
const getData = async () => {
42+
await fetchData();
43+
};
44+
getData();
45+
// eslint-disable-next-line react-hooks/exhaustive-deps
46+
}, []);
47+
48+
if (
49+
state.state === RequestState.IDLE ||
50+
state.state === RequestState.LOADING
51+
) {
52+
return <Loading />;
53+
}
54+
55+
if (state.state === RequestState.ERROR) {
56+
return (
57+
<div className="AddAsset__fetch-fail">
58+
<Notification
59+
variant="error"
60+
title={t("Failed to fetch your account data.")}
61+
>
62+
{t("Your account data could not be fetched at this time.")}
63+
</Notification>
64+
</div>
65+
);
66+
}
67+
68+
if (state.data?.type === AppDataType.REROUTE) {
69+
if (state.data.shouldOpenTab) {
70+
openTab(newTabHref(state.data.routeTarget));
71+
window.close();
72+
}
73+
return (
74+
<Navigate
75+
to={`${state.data.routeTarget}${location.search}`}
76+
state={{ from: location }}
77+
replace
78+
/>
79+
);
80+
}
81+
82+
reRouteOnboarding({
83+
type: state.data.type,
84+
applicationState: state.data.account.applicationState,
85+
state: state.state,
86+
});
87+
88+
const handleSelect = async (minutes: AutoLockTimeoutMinutes) => {
89+
if (isSaving || minutes === currentTimeout) {
90+
return;
91+
}
92+
setIsSaving(true);
93+
await dispatch(
94+
saveSettings({
95+
isDataSharingAllowed:
96+
settings.isDataSharingAllowed ?? false,
97+
isMemoValidationEnabled:
98+
settings.isMemoValidationEnabled ?? true,
99+
isHideDustEnabled: settings.isHideDustEnabled ?? true,
100+
isOpenSidebarByDefault: settings.isOpenSidebarByDefault ?? false,
101+
autoLockTimeoutMinutes: minutes,
102+
}),
103+
);
104+
setIsSaving(false);
105+
};
106+
107+
return (
108+
<React.Fragment>
109+
<SubviewHeader title={t("Auto-Lock Timer")} />
110+
<View.Content hasNoTopPadding>
111+
<div className="AutoLockTimer">
112+
{VALID_AUTO_LOCK_TIMEOUT_MINUTES.map((minutes) => {
113+
const isSelected =
114+
coerceAutoLockTimeoutMinutes(currentTimeout) === minutes;
115+
return (
116+
<React.Fragment key={minutes}>
117+
<button
118+
className={`AutoLockTimer__option${isSaving ? " AutoLockTimer__option--disabled" : ""}`}
119+
onClick={() => handleSelect(minutes)}
120+
disabled={isSaving}
121+
aria-pressed={isSelected}
122+
data-testid={`autoLockOption-${minutes}`}
123+
>
124+
<span className="AutoLockTimer__option__label">
125+
{formatTimeoutLabel(minutes, t)}
126+
</span>
127+
{isSelected && (
128+
<Icon.Check className="AutoLockTimer__option__check" />
129+
)}
130+
</button>
131+
</React.Fragment>
132+
);
133+
})}
134+
</div>
135+
</View.Content>
136+
</React.Fragment>
137+
);
138+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@use "../../styles/utils.scss" as *;
2+
3+
.AutoLockTimer {
4+
display: flex;
5+
flex-direction: column;
6+
padding: pxToRem(16px);
7+
border-radius: pxToRem(16px);
8+
background-color: var(--sds-clr-gray-02);
9+
10+
&__option {
11+
align-items: center;
12+
background: none;
13+
border: none;
14+
color: var(--sds-clr-gray-12);
15+
cursor: pointer;
16+
display: flex;
17+
justify-content: space-between;
18+
padding: pxToRem(16px) 0;
19+
text-align: left;
20+
width: 100%;
21+
font-size: pxToRem(14px);
22+
font-weight: 500;
23+
line-height: pxToRem(20px);
24+
25+
&:not(:last-child) {
26+
border-bottom: 1px solid var(--sds-clr-gray-06);
27+
}
28+
29+
&:first-child {
30+
padding-top: 0;
31+
}
32+
33+
&:last-child {
34+
padding-bottom: 0;
35+
}
36+
37+
&--disabled {
38+
cursor: default;
39+
opacity: 0.6;
40+
}
41+
42+
&__label {
43+
flex: 1;
44+
}
45+
46+
&__check {
47+
flex-shrink: 0;
48+
height: pxToRem(20px);
49+
width: pxToRem(20px);
50+
color: var(--sds-clr-gray-12);
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)