mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-25 15:36:55 +01:00
Standalone Registration: Support for Verify PIN scenarios
This commit is contained in:
@@ -9284,6 +9284,10 @@
|
||||
"messageformat": "Incorrect PIN. {triesRemaining, plural, one {# attempt remaining} other {# attempts remaining}}.",
|
||||
"description": "Error shown when entered PIN is incorrect, and "
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--just-count": {
|
||||
"messageformat": "{triesRemaining, plural, one {# attempt remaining} other {# attempts remaining}}.",
|
||||
"description": "Shown when few tries remain as the user attempts to enter the proper PIN"
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--NeedHelp--header": {
|
||||
"messageformat": "Need help?",
|
||||
"description": "Header for dialog shown when user clicks Need Help?"
|
||||
@@ -9340,6 +9344,10 @@
|
||||
"messageformat": "Learn more about PINs",
|
||||
"description": "Button for dialog shown when user has tried an incorrect PIN there are no remaining tries left"
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--open-menu": {
|
||||
"messageformat": "Open menu",
|
||||
"description": "Accessibility text for the triple-dot button on the screen asking user to verify their PIN"
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--menu--skip-pin": {
|
||||
"messageformat": "Skip PIN",
|
||||
"description": "Menu item on Verify PIN screens, allowing skip of restoring data with PIN, instead starting over from scratch with a new PIN"
|
||||
@@ -9349,7 +9357,7 @@
|
||||
"description": "Header for dialog shown when user chooses to skip PIN entry on the Verify PIN screen"
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--SkipPIN--description": {
|
||||
"messageformat": "If you can’t remember your PIN, you can create a new one. You can register and use your account but you’ll lose some saved settings like your profile information. Learn more.",
|
||||
"messageformat": "If you can’t remember your PIN, you can create a new one. You can register and use your account but you’ll lose some saved settings like your profile information. <learnMore>Learn more</learnMore>.",
|
||||
"description": "Description for dialog shown when user chooses to skip PIN entry on the Verify PIN screen"
|
||||
},
|
||||
"icu:StandaloneRegistration--VerifyPIN--SkipPIN--skip-button": {
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@
|
||||
"@react-spring/web": "10.0.3",
|
||||
"@signalapp/lame": "workspace:*",
|
||||
"@signalapp/minimask": "1.0.1",
|
||||
"@signalapp/mock-server": "24.0.3",
|
||||
"@signalapp/mock-server": "25.0.0",
|
||||
"@signalapp/parchment-cjs": "3.0.1",
|
||||
"@signalapp/quill-cjs": "2.1.2",
|
||||
"@storybook/addon-a11y": "8.4.4",
|
||||
|
||||
Generated
+5
-5
@@ -153,8 +153,8 @@ importers:
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
'@signalapp/mock-server':
|
||||
specifier: 24.0.3
|
||||
version: 24.0.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
specifier: 25.0.0
|
||||
version: 25.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
'@signalapp/parchment-cjs':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1
|
||||
@@ -4473,8 +4473,8 @@ packages:
|
||||
'@signalapp/minimask@1.0.1':
|
||||
resolution: {integrity: sha512-QAwo0joA60urTNbW9RIz6vLKQjy+jdVtH7cvY0wD9PVooD46MAjE40MLssp4xUJrph91n2XvtJ3pbEUDrmT2AA==, tarball: https://registry.npmjs.org/@signalapp/minimask/-/minimask-1.0.1.tgz}
|
||||
|
||||
'@signalapp/mock-server@24.0.3':
|
||||
resolution: {integrity: sha512-SGV0S6WvYYZRBx+F2Z+xLiTrxxtDaF8wU+orqKXWs4KU8/u1es+di6Eaoat2U9hC/eOU85hEZrpqMHVO5ad9CA==, tarball: https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-24.0.3.tgz}
|
||||
'@signalapp/mock-server@25.0.0':
|
||||
resolution: {integrity: sha512-gEOYKlwppfs8XT7feg82NQKab8XOVAaQw2rAOGth60BVpzeZI0eiTF2JoNBjVdLbJamFX8Cz2CrZAyXvJhp7Jg==, tarball: https://registry.npmjs.org/@signalapp/mock-server/-/mock-server-25.0.0.tgz}
|
||||
|
||||
'@signalapp/parchment-cjs@3.0.1':
|
||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==, tarball: https://registry.npmjs.org/@signalapp/parchment-cjs/-/parchment-cjs-3.0.1.tgz}
|
||||
@@ -15249,7 +15249,7 @@ snapshots:
|
||||
|
||||
'@signalapp/minimask@1.0.1': {}
|
||||
|
||||
'@signalapp/mock-server@24.0.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||
'@signalapp/mock-server@25.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.8.3)
|
||||
'@indutny/protopiler': 4.0.0
|
||||
|
||||
@@ -41,6 +41,12 @@ auditConfig:
|
||||
- GHSA-9wfr-w7mm-pc7f # react-devtools > electron
|
||||
- GHSA-c27g-q93r-2cwf # sticker-creator > vite
|
||||
- GHSA-2w69-qvjg-hvjx # sticker-creator > @remix-run/router
|
||||
- GHSA-23hp-3jrh-7fpw # dev-time usage
|
||||
- GHSA-3jxr-9vmj-r5cp # dev tools
|
||||
- GHSA-52cp-r559-cp3m # dev tools
|
||||
- GHSA-8x88-c5mf-7j5w # dev-time usage
|
||||
- GHSA-395f-4hp3-45gv # dev tools
|
||||
- GHSA-gcfj-64vw-6mp9 # storybook
|
||||
|
||||
overrides:
|
||||
'@electron/rebuild>node-gyp': '12.4.0'
|
||||
|
||||
@@ -212,6 +212,8 @@ export namespace AxoTextField {
|
||||
autoFocus?: boolean;
|
||||
/** Enables or disables browser spell checking. */
|
||||
spellCheck?: boolean;
|
||||
/** Default is 'text', a normal text box. Can be used to input into a password field. */
|
||||
type?: 'text' | 'password';
|
||||
}>;
|
||||
|
||||
/** The text input field. Must be placed inside `Root`. */
|
||||
@@ -305,7 +307,7 @@ export namespace AxoTextField {
|
||||
<input
|
||||
ref={mergedRef}
|
||||
id={inputId}
|
||||
type="text"
|
||||
type={props.type ?? 'text'}
|
||||
value={props.value}
|
||||
placeholder={props.placeholder ?? ''}
|
||||
required={props.required}
|
||||
|
||||
@@ -35,6 +35,7 @@ const meta = {
|
||||
} satisfies Meta<typeof StandaloneRegistration>;
|
||||
|
||||
const phoneNumber = '+14155551111';
|
||||
const verificationSessionId = 'verification-session-id';
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
@@ -171,7 +172,7 @@ export const Captcha: Story = {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -187,7 +188,7 @@ export const CaptchaInProgress: Story = {
|
||||
type: 'in-progress',
|
||||
startedAt: Date.now() - 25 * SECOND,
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -199,7 +200,7 @@ export const CaptchaWithWaitTime: Story = {
|
||||
stage: RegistrationStage.CAPTCHA,
|
||||
captchaCompleteCount: 0,
|
||||
phoneNumber,
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
status: {
|
||||
type: 'waiting',
|
||||
canDoCaptchaAt: Date.now() + MINUTE * 5,
|
||||
@@ -218,7 +219,7 @@ export const CaptchaWithAnotherNeeded: Story = {
|
||||
status: {
|
||||
type: 'another-needed',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -233,7 +234,7 @@ export const CaptchaWithFailure: Story = {
|
||||
status: {
|
||||
type: 'failed',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -249,7 +250,7 @@ export const VerificationCode: Story = {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -267,7 +268,7 @@ export const VerificationCodeWhileSubmittingCode: Story = {
|
||||
status: {
|
||||
type: 'submitting-code',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -286,7 +287,7 @@ export const VerificationCodeWhileRequestingSMS: Story = {
|
||||
type: 'requesting-code',
|
||||
transport: VerificationTransport.SMS,
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -305,7 +306,7 @@ export const VerificationCodeWhileRequestingCall: Story = {
|
||||
type: 'requesting-code',
|
||||
transport: VerificationTransport.Voice,
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -323,7 +324,7 @@ export const VerificationCodeWithFailedToSendSMS: Story = {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: true,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -340,7 +341,7 @@ export const VerificationCodeWithFailedToCall: Story = {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: true,
|
||||
},
|
||||
@@ -357,7 +358,7 @@ export const VerificationCodeWithHavingTrouble: Story = {
|
||||
type: 'ready',
|
||||
},
|
||||
phoneNumber,
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -374,7 +375,7 @@ export const VerificationCodeWithMaximumAttempts: Story = {
|
||||
type: 'ready',
|
||||
},
|
||||
phoneNumber,
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -392,7 +393,7 @@ export const VerificationCodeWithAllDelays: Story = {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
timings: {
|
||||
@@ -416,7 +417,7 @@ export const VerificationCodeWithIncorrectCode: Story = {
|
||||
type: 'failed',
|
||||
error: 'incorrect-code',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -435,7 +436,7 @@ export const VerificationCodeWithInvalidCode: Story = {
|
||||
type: 'failed',
|
||||
error: 'invalid-code',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -458,7 +459,7 @@ export const VerificationCodeWithCannotSendCodeTemporary: Story = {
|
||||
smsCanBeRequestedAt: Date.now() + 5 * MINUTE,
|
||||
codeCanBeSubmittedAt: Date.now() + 5 * MINUTE,
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -476,7 +477,7 @@ export const VerificationCodeWithCannotSendCodePermanent: Story = {
|
||||
type: 'failed',
|
||||
error: 'cannot-send-code-permanent',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
},
|
||||
@@ -546,6 +547,194 @@ export const VerifyPIN: Story = {
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINInProgress: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'in-progress',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINIncorrectPinTries3: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINIncorrectPinTries2: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINIncorrectPinTries1: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINIncorrectPinTries0: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINStartingWithTries2: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
triesRemaining: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
export const VerifyPINStartingWithTries1: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
triesRemaining: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dataForReglockAccountCreate = {
|
||||
phoneNumber,
|
||||
verificationSessionId,
|
||||
svrCredentials: {
|
||||
username: 'fake-username',
|
||||
password: 'fake-password',
|
||||
},
|
||||
profileData: {
|
||||
firstName: 'John',
|
||||
phoneNumberDiscoverability: PhoneNumberDiscoverability.Discoverable,
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglock: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglockAndIncorrectPinTries5: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 5,
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglockAndIncorrectPinTries3: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 3,
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglockAndIncorrectPinTries1: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 1,
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglockAndIncorrectPinTries0: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'incorrect-pin',
|
||||
},
|
||||
triesRemaining: 0,
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const VerifyPINWithReglockStartingWithTries1: Story = {
|
||||
args: {
|
||||
...StartingScreen.args,
|
||||
workflow: {
|
||||
stage: RegistrationStage.VERIFY_PIN,
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
triesRemaining: 1,
|
||||
dataForReglockAccountCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CreatePIN: Story = {
|
||||
args: {
|
||||
@@ -634,7 +823,7 @@ export function ProgressionWithVerify(): React.JSX.Element {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
});
|
||||
},
|
||||
moveToVerificationStage: async (...params: Array<unknown>) => {
|
||||
@@ -647,7 +836,7 @@ export function ProgressionWithVerify(): React.JSX.Element {
|
||||
status: {
|
||||
type: 'ready',
|
||||
},
|
||||
verificationSessionId: 'verification-session-id',
|
||||
verificationSessionId,
|
||||
failedToSendSMS: false,
|
||||
failedToCall: false,
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Container,
|
||||
Description,
|
||||
InputContainer,
|
||||
PIN_ARTICLE_ON_SUPPORT,
|
||||
Spacer,
|
||||
Title,
|
||||
TopMatter,
|
||||
@@ -30,6 +31,7 @@ import type {
|
||||
completeRegistration as doCompleteRegistration,
|
||||
startConfirmingPIN as doStartConfirmingPIN,
|
||||
} from '../../../state/ducks/standaloneInstaller.preload.ts';
|
||||
import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.ts';
|
||||
|
||||
const PIN_LENGTH_MINIMUM = 4;
|
||||
|
||||
@@ -60,46 +62,41 @@ export function CreatePINScreen({
|
||||
[setIsValidPIN, setPIN]
|
||||
);
|
||||
|
||||
const topRightMenu = (
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="implied-secondary" size="md">
|
||||
<AxoSymbol.Icon
|
||||
symbol="more"
|
||||
size={20}
|
||||
label={i18n('icu:StandaloneRegistration--CreatePIN--open-menu')}
|
||||
/>
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="info"
|
||||
onSelect={() => {
|
||||
setIsShowingAboutPINsDialog(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--menu--more-about')}
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="minus-circle"
|
||||
onSelect={() => {
|
||||
setIsShowingDisablePINDialog(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--menu--disable')}
|
||||
</AxoDropdownMenu.Item>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TopMatter
|
||||
i18n={i18n}
|
||||
rightContent={
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="implied-secondary" size="md">
|
||||
<AxoSymbol.Icon
|
||||
symbol="more"
|
||||
size={20}
|
||||
label={i18n(
|
||||
'icu:StandaloneRegistration--CreatePIN--open-menu'
|
||||
)}
|
||||
/>
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="info"
|
||||
onSelect={() => {
|
||||
setIsShowingAboutPINsDialog(true);
|
||||
}}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--CreatePIN--menu--more-about'
|
||||
)}
|
||||
</AxoDropdownMenu.Item>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="minus-circle"
|
||||
onSelect={() => {
|
||||
setIsShowingDisablePINDialog(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--menu--disable')}
|
||||
</AxoDropdownMenu.Item>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
}
|
||||
/>
|
||||
<TopMatter i18n={i18n} rightContent={topRightMenu} />
|
||||
<Spacer className={tw('h-13')} />
|
||||
<Title text={i18n('icu:StandaloneRegistration--CreatePIN--header')} />
|
||||
<Description className={tw('w-100')}>
|
||||
@@ -109,10 +106,7 @@ export function CreatePINScreen({
|
||||
components={{
|
||||
learnMore: parts => {
|
||||
return (
|
||||
<a
|
||||
className={tw('text-primary')}
|
||||
href="https://support.signal.org/hc/articles/360007059792-Signal-PIN"
|
||||
>
|
||||
<a className={tw('text-primary')} href={PIN_ARTICLE_ON_SUPPORT}>
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
@@ -121,8 +115,19 @@ export function CreatePINScreen({
|
||||
/>
|
||||
</Description>
|
||||
<Spacer className={tw('h-8')} />
|
||||
<InputContainer>
|
||||
<AxoTextField.Root width="sm">
|
||||
<InputContainer
|
||||
className={tw('w-81')}
|
||||
helperElement={
|
||||
<div
|
||||
className={tw(
|
||||
'ms-1 mt-1.5 w-full text-start type-body-small text-secondary'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--helper-text')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AxoTextField.Root width="lg">
|
||||
<AxoTextField.Input
|
||||
autoFocus
|
||||
maxBytes={10}
|
||||
@@ -131,13 +136,11 @@ export function CreatePINScreen({
|
||||
placeholder={i18n(
|
||||
'icu:StandaloneRegistration--CreatePIN--placeholder'
|
||||
)}
|
||||
type="password"
|
||||
value={pin}
|
||||
/>
|
||||
</AxoTextField.Root>
|
||||
</InputContainer>
|
||||
<div className={tw('mt-2 type-body-small text-secondary')}>
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--helper-text')}
|
||||
</div>
|
||||
<Spacer className={tw('grow')} />
|
||||
<Buttons>
|
||||
<AxoButton.Root
|
||||
@@ -207,9 +210,7 @@ function AboutPINsDialog({
|
||||
<AxoConfirmDialog.Action
|
||||
variant="strong-secondary"
|
||||
onClick={() => {
|
||||
// TODO: double-check location
|
||||
window.location.href =
|
||||
'https://support.signal.org/hc/articles/360007059792-Signal-PIN';
|
||||
openLinkInWebBrowser(PIN_ARTICLE_ON_SUPPORT);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -62,15 +62,17 @@ export function CreatePINConfirmScreen({
|
||||
{i18n('icu:StandaloneRegistration--CreatePIN--confirming--description')}
|
||||
</Description>
|
||||
<Spacer className={tw('h-8')} />
|
||||
<InputContainer>
|
||||
<InputContainer className={tw('w-81')}>
|
||||
<AxoTextField.Root width="md" disabled={pending}>
|
||||
<AxoTextField.Input
|
||||
autoFocus
|
||||
maxBytes={10}
|
||||
maxGraphemes={10}
|
||||
onValueChange={onChangePIN}
|
||||
placeholder={i18n(
|
||||
'icu:StandaloneRegistration--CreatePIN--confirming--placeholder'
|
||||
)}
|
||||
type="password"
|
||||
value={pin}
|
||||
/>
|
||||
</AxoTextField.Root>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Buttons,
|
||||
Container,
|
||||
Description,
|
||||
InputContainer,
|
||||
Spacer,
|
||||
Title,
|
||||
TopMatter,
|
||||
@@ -99,7 +100,7 @@ export function PhoneNumberScreen({
|
||||
</div>
|
||||
</Description>
|
||||
<Spacer className={tw('h-9')} />
|
||||
<div className={tw('w-81 max-w-[calc(100%-48px)]')}>
|
||||
<InputContainer className={tw('w-81')}>
|
||||
<AxoTextField.Root width="lg">
|
||||
{regionCode ? (
|
||||
<div className={tw('p-1.5 ps-3 type-body-large text-primary')}>
|
||||
@@ -125,7 +126,7 @@ export function PhoneNumberScreen({
|
||||
value={phoneNumber ?? ''}
|
||||
/>
|
||||
</AxoTextField.Root>
|
||||
</div>
|
||||
</InputContainer>
|
||||
<Spacer className={tw('grow')} />
|
||||
{status.type === 'waiting' && duration ? (
|
||||
<div>
|
||||
|
||||
@@ -96,7 +96,10 @@ export function ProfileEntryScreen({
|
||||
{i18n('icu:StandaloneRegistration--ProfileEntry--description')}
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://support.signal.org">
|
||||
<a
|
||||
className={tw('text-primary')}
|
||||
href="https://support.signal.org/hc/articles/360007459591-Signal-Profiles-and-Message-Requests"
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--ProfileEntry--learn-more')}
|
||||
</a>
|
||||
</div>
|
||||
@@ -134,7 +137,7 @@ export function ProfileEntryScreen({
|
||||
{i18n('icu:StandaloneRegistration--ProfileEntry--add-photo')}
|
||||
</AxoButton.Root>
|
||||
<Spacer className={tw('h-7')} />
|
||||
<InputContainer>
|
||||
<InputContainer className={tw('w-100')}>
|
||||
<AxoTextField.Root width="md" disabled={pending}>
|
||||
<AxoTextField.Input
|
||||
placeholder={i18n(
|
||||
@@ -148,7 +151,7 @@ export function ProfileEntryScreen({
|
||||
</AxoTextField.Root>
|
||||
</InputContainer>
|
||||
<Spacer />
|
||||
<InputContainer>
|
||||
<InputContainer className={tw('w-100')}>
|
||||
<AxoTextField.Root width="md" disabled={pending}>
|
||||
<AxoTextField.Input
|
||||
placeholder={i18n(
|
||||
|
||||
@@ -151,13 +151,17 @@ export function VerificationCodeScreen({
|
||||
timings || {};
|
||||
const updateCodeSubmitCountdown = useCallback(
|
||||
(newDuration: string | undefined) => {
|
||||
if (!waitSubmitDuration && newDuration) {
|
||||
if (
|
||||
!waitSubmitDuration &&
|
||||
newDuration &&
|
||||
workflow.status.type !== 'failed' // we have error-specific dialogs
|
||||
) {
|
||||
setMustWaitDialogOpen(true);
|
||||
}
|
||||
|
||||
setWaitSubmitDuration(newDuration);
|
||||
},
|
||||
[waitSubmitDuration, setWaitSubmitDuration, setMustWaitDialogOpen]
|
||||
[setMustWaitDialogOpen, setWaitSubmitDuration, waitSubmitDuration, workflow]
|
||||
);
|
||||
useCountdownDuration({
|
||||
timestamp: codeCanBeSubmittedAt,
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
|
||||
import type { ChangeEvent, JSX } from 'react';
|
||||
import type { JSX, ReactNode } from 'react';
|
||||
|
||||
import { I18n } from '../../I18n.dom.tsx';
|
||||
import { AxoButton } from '../../../axo/AxoButton.dom.tsx';
|
||||
import { tw } from '../../../axo/tw.dom.tsx';
|
||||
import {
|
||||
Buttons,
|
||||
Container,
|
||||
Description,
|
||||
InputContainer,
|
||||
PIN_ARTICLE_ON_SUPPORT,
|
||||
Spacer,
|
||||
Title,
|
||||
TopMatter,
|
||||
} from '../util/StepComponents.dom.tsx';
|
||||
|
||||
import type { LocalizerType } from '../../../types/I18N.std.ts';
|
||||
import type { ActionCreator } from '../../../state/types.std.ts';
|
||||
@@ -16,6 +27,18 @@ import type {
|
||||
goToCreatePINStage as doGoToCreatePINStage,
|
||||
verifyPIN as doVerifyPIN,
|
||||
} from '../../../state/ducks/standaloneInstaller.preload.ts';
|
||||
import { AxoTextField } from '../../../axo/AxoTextField.dom.tsx';
|
||||
import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.tsx';
|
||||
import { openLinkInWebBrowser } from '../../../util/openLinkInWebBrowser.dom.ts';
|
||||
import { CONTACT_SUPPORT_URL } from '../../../util/contactSupport.dom.tsx';
|
||||
import { AxoDropdownMenu } from '../../../axo/AxoDropdownMenu.dom.tsx';
|
||||
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.tsx';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
const FIRST_REGLOCK_TRIES_THRESHOLD = 5;
|
||||
const SECOND_REGLOCK_TRIES_THRESHOLD = 3;
|
||||
const FIRST_NON_REGLOCK_TRIES_THRESHOLD = 3;
|
||||
const SECOND_NON_REGLOCK_TRIES_THRESHOLD = 1;
|
||||
|
||||
export function VerifyPINScreen({
|
||||
verifyPIN,
|
||||
@@ -32,112 +55,536 @@ export function VerifyPINScreen({
|
||||
}): JSX.Element {
|
||||
const [pin, setPIN] = useState('');
|
||||
const [isValidPIN, setIsValidPIN] = useState(false);
|
||||
const [needHelpDialogOpen, setNeedHelpDialogOpen] = useState(false);
|
||||
const [skipPINDialogOpen, setSkipPINDialogOpen] = useState(false);
|
||||
const [fewRemainingTriesDialogOpen, setFewRemainingTriesDialogOpen] =
|
||||
useState(false);
|
||||
const [noRemainingTriesDialogOpen, setNoRemainingTriesDialogOpen] =
|
||||
useState(false);
|
||||
|
||||
const [shouldShowIncorrectPIN, setShouldShowIncorrectPIN] = useState<
|
||||
| { type: 'error' }
|
||||
| { type: 'error-with-count'; count: number }
|
||||
| { type: 'count'; count: number }
|
||||
| undefined
|
||||
>(undefined);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onChangePIN = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
(value: string) => {
|
||||
inputRef.current?.setCustomValidity('');
|
||||
setIsValidPIN(value.length === 6);
|
||||
setPIN(value);
|
||||
},
|
||||
[setIsValidPIN, setPIN]
|
||||
);
|
||||
|
||||
// TODO: show below input box if entered PIN is incorrect
|
||||
// icu:StandaloneRegistration--VerifyPIN--incorrect-pin
|
||||
// icu:StandaloneRegistration--VerifyPIN--incorrect-pin--with-count
|
||||
const { status } = workflow;
|
||||
const pending = status.type === 'in-progress';
|
||||
const isIncorrectPin =
|
||||
status.type === 'failed' && status.error === 'incorrect-pin';
|
||||
|
||||
// TODO: Need Help dialog
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--header
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--description--no-reglock
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--description--reglock
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--skip-button--no-reglock
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--contact-support-button
|
||||
// icu:StandaloneRegistration--VerifyPIN--NeedHelp--cancel
|
||||
const hasReglock = Boolean(workflow.dataForReglockAccountCreate);
|
||||
|
||||
// TODO: Incorrect pin, N attempts remaining dialog
|
||||
// icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--header
|
||||
// icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--description--no-reglock
|
||||
// icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--description--reglock
|
||||
// icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--button
|
||||
const menu = hasReglock ? undefined : (
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="implied-secondary" size="md">
|
||||
<AxoSymbol.Icon
|
||||
symbol="more"
|
||||
size={20}
|
||||
label={i18n('icu:StandaloneRegistration--VerifyPIN--open-menu')}
|
||||
/>
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<AxoDropdownMenu.Content>
|
||||
<AxoDropdownMenu.Item
|
||||
symbol="arrow-circle-[end]"
|
||||
onSelect={() => {
|
||||
setSkipPINDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--menu--skip-pin')}
|
||||
</AxoDropdownMenu.Item>
|
||||
</AxoDropdownMenu.Content>
|
||||
</AxoDropdownMenu.Root>
|
||||
);
|
||||
|
||||
// TODO: Incorrect PIN, no attempts remaining dialog
|
||||
// icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--header
|
||||
// icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--description
|
||||
// icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--create-new-pin
|
||||
// icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--learn-more
|
||||
const { triesRemaining } = workflow;
|
||||
const previousTriesRemaining = useRef<number | undefined>(undefined);
|
||||
|
||||
// TODO: menu with one item:
|
||||
// icu:StandaloneRegistration--VerifyPIN--menu--skip-pin
|
||||
let helperElement: ReactNode | undefined;
|
||||
|
||||
// TODO: icu:StandaloneRegistration--VerifyPIN--SkipPIN--header
|
||||
// icu:StandaloneRegistration--VerifyPIN--SkipPIN--description
|
||||
// icu:StandaloneRegistration--VerifyPIN--SkipPIN--skip-button
|
||||
// icu:StandaloneRegistration--VerifyPIN--SkipPIN--cancel-button
|
||||
if (
|
||||
previousTriesRemaining.current !== triesRemaining &&
|
||||
isNumber(triesRemaining)
|
||||
) {
|
||||
previousTriesRemaining.current = triesRemaining;
|
||||
|
||||
if (
|
||||
hasReglock &&
|
||||
(triesRemaining === FIRST_REGLOCK_TRIES_THRESHOLD ||
|
||||
triesRemaining === SECOND_REGLOCK_TRIES_THRESHOLD)
|
||||
) {
|
||||
setFewRemainingTriesDialogOpen(true);
|
||||
}
|
||||
if (hasReglock && triesRemaining === 0) {
|
||||
goToAccountLockedStage();
|
||||
}
|
||||
|
||||
if (
|
||||
!hasReglock &&
|
||||
(triesRemaining === FIRST_NON_REGLOCK_TRIES_THRESHOLD ||
|
||||
triesRemaining === SECOND_NON_REGLOCK_TRIES_THRESHOLD)
|
||||
) {
|
||||
setFewRemainingTriesDialogOpen(true);
|
||||
}
|
||||
if (!hasReglock && triesRemaining === 0) {
|
||||
setNoRemainingTriesDialogOpen(true);
|
||||
}
|
||||
|
||||
if (
|
||||
(hasReglock && triesRemaining <= FIRST_REGLOCK_TRIES_THRESHOLD) ||
|
||||
(!hasReglock && triesRemaining <= FIRST_NON_REGLOCK_TRIES_THRESHOLD)
|
||||
) {
|
||||
if (isIncorrectPin) {
|
||||
inputRef.current?.setCustomValidity('invalid');
|
||||
setShouldShowIncorrectPIN({
|
||||
type: 'error-with-count',
|
||||
count: triesRemaining,
|
||||
});
|
||||
} else {
|
||||
inputRef.current?.setCustomValidity('invalid');
|
||||
setShouldShowIncorrectPIN({
|
||||
type: 'count',
|
||||
count: triesRemaining,
|
||||
});
|
||||
}
|
||||
} else if (isIncorrectPin) {
|
||||
inputRef.current?.setCustomValidity('invalid');
|
||||
setShouldShowIncorrectPIN({ type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldShowIncorrectPIN?.type === 'count') {
|
||||
helperElement = (
|
||||
<div
|
||||
className={tw(
|
||||
'ms-1 mt-1.5 w-full text-start type-body-small text-destructive'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--just-count', {
|
||||
triesRemaining: shouldShowIncorrectPIN.count,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (shouldShowIncorrectPIN?.type === 'error-with-count') {
|
||||
helperElement = (
|
||||
<div
|
||||
className={tw(
|
||||
'ms-1 mt-1.5 w-full text-start type-body-small text-destructive'
|
||||
)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--incorrect-pin--with-count',
|
||||
{ triesRemaining: shouldShowIncorrectPIN.count }
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (shouldShowIncorrectPIN?.type === 'error') {
|
||||
helperElement = (
|
||||
<div
|
||||
className={tw(
|
||||
'ms-1 mt-1.5 w-full text-start type-body-small text-destructive'
|
||||
)}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--incorrect-pin')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="step-body">
|
||||
<div className="header">
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--header')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--description"
|
||||
components={{
|
||||
needHelp: parts => {
|
||||
return (
|
||||
<a href="https://support.signal.org/hc/articles/360007059792-Signal-PIN">
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* FIXME */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */}
|
||||
<input
|
||||
className={`form-control ${isValidPIN ? 'valid' : 'invalid'}`}
|
||||
type="text"
|
||||
dir="auto"
|
||||
pattern="\s*[0-9]{3}-?[0-9]{3}\s*"
|
||||
title="Enter your 6-digit verification code. If you did not receive a code, click Call or Send SMS to request a new one"
|
||||
placeholder={i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--placeholder'
|
||||
)}
|
||||
autoComplete="off"
|
||||
value={pin}
|
||||
onChange={onChangePIN}
|
||||
<Container>
|
||||
<TopMatter i18n={i18n} rightContent={menu} />
|
||||
<Spacer className={tw('h-14')} />
|
||||
<Title text={i18n('icu:StandaloneRegistration--VerifyPIN--header')} />
|
||||
<Description>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--description"
|
||||
components={{
|
||||
needHelp: parts => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={tw('text-primary')}
|
||||
onClick={e => {
|
||||
setNeedHelpDialogOpen(true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setNeedHelpDialogOpen(true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{parts}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="nav">
|
||||
<AxoButton.Root
|
||||
variant="implied-primary"
|
||||
size="md"
|
||||
onClick={() => goToCreatePINStage()}
|
||||
>
|
||||
TODO: Create new PIN
|
||||
</AxoButton.Root>
|
||||
<AxoButton.Root
|
||||
variant="implied-primary"
|
||||
size="md"
|
||||
onClick={() => goToAccountLockedStage()}
|
||||
>
|
||||
TODO: Account is locked!
|
||||
</AxoButton.Root>
|
||||
|
||||
</Description>
|
||||
<Spacer className={tw('h-10')} />
|
||||
<InputContainer className={tw('w-81')} helperElement={helperElement}>
|
||||
<AxoTextField.Root width="lg">
|
||||
<AxoTextField.Input
|
||||
autoFocus
|
||||
maxBytes={10}
|
||||
maxGraphemes={10}
|
||||
onValueChange={onChangePIN}
|
||||
disabled={pending}
|
||||
placeholder={i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--placeholder'
|
||||
)}
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
value={pin}
|
||||
/>
|
||||
</AxoTextField.Root>
|
||||
</InputContainer>
|
||||
<Spacer className={tw('grow')} />
|
||||
<Buttons>
|
||||
<AxoButton.Root
|
||||
variant="strong-primary"
|
||||
size="md"
|
||||
disabled={!isValidPIN}
|
||||
onClick={() => verifyPIN({ pin, workflow })}
|
||||
pending={pending}
|
||||
disabled={!isValidPIN || pending}
|
||||
onClick={() => {
|
||||
setShouldShowIncorrectPIN(undefined);
|
||||
verifyPIN({ pin, workflow });
|
||||
}}
|
||||
>
|
||||
TODO: Finish
|
||||
{i18n('icu:StandaloneRegistration--ProfileEntry--continue')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</>
|
||||
</Buttons>
|
||||
<NeedHelpDialog
|
||||
i18n={i18n}
|
||||
isOpen={needHelpDialogOpen}
|
||||
setOpen={setNeedHelpDialogOpen}
|
||||
hasReglock={hasReglock}
|
||||
goToCreatePINStage={goToCreatePINStage}
|
||||
/>
|
||||
<SkipPINDialog
|
||||
i18n={i18n}
|
||||
isOpen={skipPINDialogOpen}
|
||||
setOpen={setSkipPINDialogOpen}
|
||||
goToCreatePINStage={goToCreatePINStage}
|
||||
/>
|
||||
<FewRemainingTriesDialog
|
||||
i18n={i18n}
|
||||
isOpen={fewRemainingTriesDialogOpen}
|
||||
setOpen={setFewRemainingTriesDialogOpen}
|
||||
hasReglock={hasReglock}
|
||||
triesRemaining={triesRemaining ?? 0}
|
||||
/>
|
||||
<NoRemainingTriesDialog
|
||||
i18n={i18n}
|
||||
isOpen={noRemainingTriesDialogOpen}
|
||||
setOpen={setNoRemainingTriesDialogOpen}
|
||||
goToCreatePINStage={goToCreatePINStage}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const learnMore = (parts: ReactNode) => {
|
||||
return (
|
||||
<a className={tw('text-primary')} href={PIN_ARTICLE_ON_SUPPORT}>
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
function NeedHelpDialog({
|
||||
// Housekeeping
|
||||
i18n,
|
||||
isOpen,
|
||||
setOpen,
|
||||
// Specifics
|
||||
hasReglock,
|
||||
goToCreatePINStage,
|
||||
}: {
|
||||
// Housekeeping
|
||||
i18n: LocalizerType;
|
||||
isOpen: boolean;
|
||||
setOpen: (value: boolean) => unknown;
|
||||
// Specifics
|
||||
hasReglock: boolean;
|
||||
goToCreatePINStage: ActionCreator<typeof doGoToCreatePINStage>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<AxoAlertDialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
<div className={tw('text-start')}>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--NeedHelp--header')}
|
||||
</div>
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
<div className={tw('text-start')}>
|
||||
{hasReglock ? (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--NeedHelp--description--reglock"
|
||||
components={{ learnMore }}
|
||||
/>
|
||||
) : (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--NeedHelp--description--no-reglock"
|
||||
components={{ learnMore }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer forceAlwaysBreakToSeparateLines>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-secondary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--NeedHelp--cancel')}
|
||||
</AxoAlertDialog.Action>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-secondary"
|
||||
onClick={() => openLinkInWebBrowser(CONTACT_SUPPORT_URL)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NeedHelp--contact-support-button'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
{hasReglock ? undefined : (
|
||||
<AxoAlertDialog.Action
|
||||
variant="subtle-destructive"
|
||||
onClick={() => goToCreatePINStage()}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NeedHelp--skip-button--no-reglock'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
)}
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
);
|
||||
}
|
||||
function SkipPINDialog({
|
||||
// Housekeeping
|
||||
i18n,
|
||||
isOpen,
|
||||
setOpen,
|
||||
// Specifics
|
||||
goToCreatePINStage,
|
||||
}: {
|
||||
// Housekeeping
|
||||
i18n: LocalizerType;
|
||||
isOpen: boolean;
|
||||
setOpen: (value: boolean) => unknown;
|
||||
// Specifics
|
||||
goToCreatePINStage: ActionCreator<typeof doGoToCreatePINStage>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<AxoAlertDialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
<div className={tw('text-start')}>
|
||||
{i18n('icu:StandaloneRegistration--VerifyPIN--SkipPIN--header')}
|
||||
</div>
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
<div className={tw('text-start')}>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--SkipPIN--description"
|
||||
components={{
|
||||
learnMore,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer forceAlwaysBreakToSeparateLines>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-secondary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--SkipPIN--cancel-button'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
<AxoAlertDialog.Action
|
||||
variant="subtle-destructive"
|
||||
onClick={() => goToCreatePINStage()}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--SkipPIN--skip-button'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function FewRemainingTriesDialog({
|
||||
// Housekeeping
|
||||
i18n,
|
||||
isOpen,
|
||||
setOpen,
|
||||
// Specifics
|
||||
hasReglock,
|
||||
triesRemaining,
|
||||
}: {
|
||||
// Housekeeping
|
||||
i18n: LocalizerType;
|
||||
isOpen: boolean;
|
||||
setOpen: (value: boolean) => unknown;
|
||||
// Specifics
|
||||
hasReglock: boolean;
|
||||
triesRemaining: number;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<AxoAlertDialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
<div className={tw('text-start')}>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--header'
|
||||
)}
|
||||
</div>
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
<div className={tw('text-start')}>
|
||||
{hasReglock ? (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--description--reglock"
|
||||
components={{ learnMore, triesRemaining }}
|
||||
/>
|
||||
) : (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--description--no-reglock"
|
||||
components={{ learnMore, triesRemaining }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer forceAlwaysBreakToSeparateLines>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-primary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--FewRemainingTries--button'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NoRemainingTriesDialog({
|
||||
// Housekeeping
|
||||
i18n,
|
||||
isOpen,
|
||||
setOpen,
|
||||
// Specifics
|
||||
goToCreatePINStage,
|
||||
}: {
|
||||
// Housekeeping
|
||||
i18n: LocalizerType;
|
||||
isOpen: boolean;
|
||||
setOpen: (value: boolean) => unknown;
|
||||
// Specifics
|
||||
goToCreatePINStage: ActionCreator<typeof doGoToCreatePINStage>;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<AxoAlertDialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={open => {
|
||||
if (!open) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AxoAlertDialog.Content escape="cancel-is-noop">
|
||||
<AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Title>
|
||||
<div className={tw('text-start')}>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--header'
|
||||
)}
|
||||
</div>
|
||||
</AxoAlertDialog.Title>
|
||||
<AxoAlertDialog.Description>
|
||||
<div className={tw('text-start')}>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--description'
|
||||
)}
|
||||
</div>
|
||||
</AxoAlertDialog.Description>
|
||||
</AxoAlertDialog.Body>
|
||||
<AxoAlertDialog.Footer forceAlwaysBreakToSeparateLines>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-secondary"
|
||||
onClick={() => openLinkInWebBrowser(PIN_ARTICLE_ON_SUPPORT)}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--learn-more'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
<AxoAlertDialog.Action
|
||||
variant="strong-primary"
|
||||
onClick={() => goToCreatePINStage()}
|
||||
>
|
||||
{i18n(
|
||||
'icu:StandaloneRegistration--VerifyPIN--NoRemainingTries--create-new-pin'
|
||||
)}
|
||||
</AxoAlertDialog.Action>
|
||||
</AxoAlertDialog.Footer>
|
||||
</AxoAlertDialog.Content>
|
||||
</AxoAlertDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import { tw } from '../../../axo/tw.dom.tsx';
|
||||
import type { LocalizerType } from '../../../types/I18N.std.ts';
|
||||
import { AxoIconButton } from '../../../axo/AxoIconButton.dom.tsx';
|
||||
|
||||
export const PIN_ARTICLE_ON_SUPPORT =
|
||||
'https://support.signal.org/hc/articles/360007059792-Signal-PIN';
|
||||
|
||||
export function Container({
|
||||
children,
|
||||
className,
|
||||
@@ -56,12 +59,22 @@ export function TopMatter({
|
||||
|
||||
export function InputContainer({
|
||||
children,
|
||||
className,
|
||||
helperElement,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
helperElement?: ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex min-h-fit w-full flex-col items-center')}>
|
||||
<div
|
||||
className={classNames(
|
||||
tw('flex min-h-fit max-w-full flex-col items-center'),
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{helperElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -795,21 +795,14 @@ export function verifyPIN({
|
||||
};
|
||||
dispatch(updateWorkflow(workflow));
|
||||
} else if (error === 'missing') {
|
||||
log.warn(`${logId}: Nothing in SVR for this user!`);
|
||||
log.error(`${logId}: Nothing in SVR for this user!`);
|
||||
|
||||
if (dataForReglockAccountCreate) {
|
||||
// Something has really gone wrong - we're in reglock, but SVR has nothing for us
|
||||
dispatch(updateWorkflow(workflow, FatalErrorType.UNEXPECTED));
|
||||
} else {
|
||||
// No reglock and nothing in SVR - let's allow the user to create a new PIN
|
||||
workflow = {
|
||||
...workflow,
|
||||
status: {
|
||||
type: 'failed',
|
||||
error: 'nothing-in-svr',
|
||||
},
|
||||
};
|
||||
dispatch(updateWorkflow(workflow));
|
||||
dispatch(goToCreatePINStage());
|
||||
}
|
||||
} else {
|
||||
const unknownError: never = error;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Bootstrap } from '../bootstrap.node.ts';
|
||||
import { MINUTE } from '../../util/durations/index.std.ts';
|
||||
import { typeIntoInput } from '../helpers.node.ts';
|
||||
import { assert } from 'chai';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
export const debug = createDebug('mock:test:registration');
|
||||
|
||||
@@ -34,9 +35,7 @@ describe('registration', function (this: Mocha.Suite) {
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('should create totally new account, creating a new PIN with SVR2', async () => {
|
||||
this.timeout(10000);
|
||||
|
||||
it('should create new account, creating a new PIN with SVR2', async () => {
|
||||
const window = await app.getWindow();
|
||||
|
||||
{
|
||||
@@ -71,9 +70,9 @@ describe('registration', function (this: Mocha.Suite) {
|
||||
|
||||
{
|
||||
debug('VERIFICATION_CODE: enter code');
|
||||
const code = '111111';
|
||||
for (let i = 0; i < code.length; i += 1) {
|
||||
const char = code[i];
|
||||
const CODE = '111111';
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
const char = CODE[i];
|
||||
if (!char) {
|
||||
continue;
|
||||
}
|
||||
@@ -90,32 +89,151 @@ describe('registration', function (this: Mocha.Suite) {
|
||||
debug('PROFILE_ENTRY: enter first name');
|
||||
const firstNameInput = window.getByPlaceholder('First name (required)');
|
||||
await typeIntoInput(firstNameInput, 'John', '');
|
||||
}
|
||||
|
||||
{
|
||||
debug('PROFILE_ENTRY: complete');
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
const pin = '876543';
|
||||
const PIN = '876543';
|
||||
|
||||
{
|
||||
debug('CREATE_PIN: enter pin');
|
||||
const phoneInput = window.getByPlaceholder('Create your PIN');
|
||||
await typeIntoInput(phoneInput, pin, '');
|
||||
await typeIntoInput(phoneInput, PIN, '');
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('CREATE_PIN_CONFIRM: enter pin again');
|
||||
const phoneInput = window.getByPlaceholder('Enter your PIN');
|
||||
await typeIntoInput(phoneInput, pin, '');
|
||||
await typeIntoInput(phoneInput, PIN, '');
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
const storedData = await app.getSvr2StoreParameters();
|
||||
assert.strictEqual(storedData?.pin, pin);
|
||||
{
|
||||
debug('COMPLETE: Verify data was stored in SVR');
|
||||
const storedData = await app.getSvr2StoreParameters();
|
||||
assert.strictEqual(storedData?.pin, PIN);
|
||||
}
|
||||
|
||||
await expect(window.getByText('Welcome to Signal')).toBeVisible();
|
||||
{
|
||||
debug('COMPLETE: verify welcome screen');
|
||||
await expect(window.getByText('Welcome to Signal')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
it('should reregister account, verifying PIN with SVR2', async () => {
|
||||
const window = await app.getWindow();
|
||||
const { server } = bootstrap;
|
||||
|
||||
{
|
||||
debug('PHONE_NUMBER: Enter phone number');
|
||||
const phoneInput = window.getByPlaceholder('Phone number');
|
||||
await typeIntoInput(phoneInput, '+14155551111', '');
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('PHONE_NUMBER: Confirm phone number');
|
||||
const dialogText = window.getByText(
|
||||
'Is your phone number above correct?'
|
||||
);
|
||||
await expect(dialogText).toBeVisible();
|
||||
|
||||
await window.getByRole('button', { name: 'Yes' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('CAPTCHA: kick off validation');
|
||||
await window.getByRole('button', { name: 'Verify in Browser' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('CAPTCHA: complete validation');
|
||||
const { seq, reason } = await app.waitForChallenge();
|
||||
assert.strictEqual(reason, 'standalone registration');
|
||||
|
||||
await app.solveChallenge({ seq, data: { captcha: 'unused' } });
|
||||
}
|
||||
|
||||
{
|
||||
debug('VERIFICATION_CODE: enter code');
|
||||
const CODE = '111111';
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
const char = CODE[i];
|
||||
if (!char) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const codeInput = window.getByLabel(`Character ${i + 1} of 6`);
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
await typeIntoInput(codeInput, char, '');
|
||||
}
|
||||
|
||||
// Force server to return storageCapable: true
|
||||
server.setRegisterResponseData({ storageCapable: true });
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('PROFILE_ENTRY: enter first name');
|
||||
const firstNameInput = window.getByPlaceholder('First name (required)');
|
||||
await typeIntoInput(firstNameInput, 'John', '');
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('VERIFY_PIN: enter incorrect PIN');
|
||||
|
||||
const INCORRECT_PIN = '123456';
|
||||
|
||||
const phoneInput = window.getByPlaceholder('Enter your PIN');
|
||||
await typeIntoInput(phoneInput, INCORRECT_PIN, '');
|
||||
|
||||
await app.saveSVR2RestoreResponse({
|
||||
success: false,
|
||||
error: 'pin-incorrect',
|
||||
triesRemaining: 3,
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('VERIFY_PIN: dismiss dialog');
|
||||
await expect(
|
||||
window.getByText('You have 3 attempts remaining')
|
||||
).toBeVisible();
|
||||
|
||||
await window.getByRole('button', { name: 'OK' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('VERIFY_PIN: enter correct PIN');
|
||||
|
||||
const CORRECT_PIN = '876543';
|
||||
const DATA = randomBytes(32);
|
||||
|
||||
const phoneInput = window.getByPlaceholder('Enter your PIN');
|
||||
|
||||
await phoneInput.clear();
|
||||
await typeIntoInput(phoneInput, CORRECT_PIN, '');
|
||||
|
||||
await app.saveSVR2RestoreResponse({
|
||||
success: true,
|
||||
data: DATA,
|
||||
triesRemaining: 3,
|
||||
});
|
||||
|
||||
await window.getByRole('button', { name: 'Continue' }).click();
|
||||
}
|
||||
|
||||
{
|
||||
debug('COMPLETE: verify welcome screen');
|
||||
await expect(window.getByText('Welcome to Signal')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +312,7 @@ export type VerifyPINStage = {
|
||||
| {
|
||||
type: 'in-progress';
|
||||
}
|
||||
| { type: 'failed'; error: 'nothing-in-svr' };
|
||||
| { type: 'failed'; error: 'incorrect-pin' };
|
||||
|
||||
// After VERIFY_PIN:
|
||||
// - save PIN in local storage
|
||||
|
||||
Reference in New Issue
Block a user