Skip to content

Commit

Permalink
wallet-ext: add zkLogin 2fa modal (MystenLabs#13958)
Browse files Browse the repository at this point in the history
## Description 

for every zkLogin account show a modal to the user to enable 2fa



https://github.com/MystenLabs/sui/assets/10210143/d1b0f165-c9b6-4b30-9d6b-26d43d924906

closes [APPS-1746](https://mysten.atlassian.net/browse/APPS-1746)

## Test Plan 

How did you test the new or updated feature?

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
pchrysochoidis authored Sep 25, 2023
1 parent 4904e8d commit 5130ade
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 5 deletions.
3 changes: 3 additions & 0 deletions apps/core/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export default {
offwhite: '#fefefe',
offblack: '#111111',
ebony: '#101828',
avocado: {
200: '#CBE5BE',
},
},

extend: {
Expand Down
17 changes: 16 additions & 1 deletion apps/wallet/src/background/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ImportedAccount } from './ImportedAccount';
import { LedgerAccount } from './LedgerAccount';
import { MnemonicAccount } from './MnemonicAccount';
import { QredoAccount } from './QredoAccount';
import { ZkAccount } from './zk/ZkAccount';
import { ZkAccount, type ZkAccountSerialized } from './zk/ZkAccount';

function toAccount(account: SerializedAccount) {
if (MnemonicAccount.isOfType(account)) {
Expand Down Expand Up @@ -362,5 +362,20 @@ export async function accountsHandleUIMessage(msg: Message, uiConnection: UiConn
await uiConnection.send(createMessage({ type: 'done' }, msg.id));
return true;
}
if (isMethodPayload(payload, 'acknowledgeZkLoginWarning')) {
const { accountID } = payload.args;
const account = await getAccountByID(accountID);
if (!account) {
throw new Error(`Account with id ${accountID} not found.`);
}
if (!(account instanceof ZkAccount)) {
throw new Error(`Account with id ${accountID} is not a zkLogin account.`);
}
const updates: Partial<ZkAccountSerialized> = { warningAcknowledged: true };
await (await getDB()).accounts.update(accountID, updates);
accountsEvents.emit('accountStatusChanged', { accountID });
await uiConnection.send(createMessage({ type: 'done' }, msg.id));
return true;
}
return false;
}
5 changes: 4 additions & 1 deletion apps/wallet/src/background/accounts/zk/ZkAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,15 @@ export interface ZkAccountSerialized extends SerializedAccount {
* the name/key of the claim in claims used for the address sub or email
*/
claimName: 'sub' | 'email';
warningAcknowledged?: boolean;
}

export interface ZkAccountSerializedUI extends SerializedUIAccount {
type: 'zk';
email: string | null;
picture: string | null;
provider: ZkProvider;
warningAcknowledged: boolean;
}

export function isZkAccountSerializedUI(
Expand Down Expand Up @@ -174,7 +176,7 @@ export class ZkAccount
}

async toUISerialized(): Promise<ZkAccountSerializedUI> {
const { address, publicKey, type, claims, selected, provider, nickname } =
const { address, publicKey, type, claims, selected, provider, nickname, warningAcknowledged } =
await this.getStoredData();
const { email, picture } = await deobfuscate<JwtSerializedClaims>(claims);
return {
Expand All @@ -191,6 +193,7 @@ export class ZkAccount
isPasswordUnlockable: false,
provider,
isKeyPairExportable: false,
warningAcknowledged: !!warningAcknowledged,
};
}

Expand Down
4 changes: 4 additions & 0 deletions apps/wallet/src/background/accounts/zk/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ZkProviderData {
}) => void;
enabled: boolean;
hidden?: boolean;
mfaLink?: string;
}

const isDev = process.env.NODE_ENV === 'development';
Expand All @@ -35,6 +36,7 @@ export const zkProviderDataMap: Record<ZkProvider, ZkProviderData> = {
}
},
enabled: true,
mfaLink: 'https://support.google.com/accounts/answer/185839',
},
twitch: {
clientID: 'uzpfot3uotf7fp9hklsyctn2735bcw',
Expand All @@ -57,6 +59,7 @@ export const zkProviderDataMap: Record<ZkProvider, ZkProviderData> = {
}
},
enabled: true,
mfaLink: 'https://help.twitch.tv/s/article/two-factor-authentication',
},
facebook: {
clientID: '829226485248571',
Expand All @@ -67,5 +70,6 @@ export const zkProviderDataMap: Record<ZkProvider, ZkProviderData> = {
},
enabled: isDev,
hidden: !isDev,
mfaLink: 'https://www.facebook.com/help/148233965247823',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type MethodPayloads = {
data: PasswordRecoveryData;
};
removeAccount: { accountID: string };
acknowledgeZkLoginWarning: { accountID: string };
};

type Methods = keyof MethodPayloads;
Expand Down
12 changes: 12 additions & 0 deletions apps/wallet/src/ui/app/background-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,18 @@ export class BackgroundClient {
);
}

public acknowledgeZkLoginWarning(args: MethodPayload<'acknowledgeZkLoginWarning'>['args']) {
return lastValueFrom(
this.sendMessage(
createMessage<MethodPayload<'acknowledgeZkLoginWarning'>>({
type: 'method-payload',
method: 'acknowledgeZkLoginWarning',
args,
}),
).pipe(take(1)),
);
}

private loadFeatures() {
return lastValueFrom(
this.sendMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { zkProviderDataMap, type ZkProvider } from '_src/background/accounts/zk/providers';
import { isZkAccountSerializedUI } from '_src/background/accounts/zk/ZkAccount';
import { type MethodPayload } from '_src/shared/messaging/messages/payloads/MethodPayload';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '_src/ui/app/shared/Dialog';
import { useMutation } from '@tanstack/react-query';
import toast from 'react-hot-toast';

import { useActiveAccount } from '../../hooks/useActiveAccount';
import { useBackgroundClient } from '../../hooks/useBackgroundClient';
import { Button } from '../../shared/ButtonUI';
import { Link } from '../../shared/Link';

const providerToName: Record<ZkProvider, string> = {
google: 'Google',
facebook: 'Facebook',
twitch: 'Twitch',
};

export function ZkLoginAccountWarningModal() {
const activeAccount = useActiveAccount();
const backgroundClient = useBackgroundClient();
const warningMutation = useMutation({
mutationKey: ['acknowledge-zk-login-warning'],
mutationFn: (args: MethodPayload<'acknowledgeZkLoginWarning'>['args']) =>
backgroundClient.acknowledgeZkLoginWarning(args),
});
if (
activeAccount &&
isZkAccountSerializedUI(activeAccount) &&
!activeAccount.warningAcknowledged
) {
const providerData = zkProviderDataMap[activeAccount.provider];
return (
<Dialog open>
<DialogContent onPointerDownOutside={(e) => e.preventDefault()} background="avocado">
<DialogHeader>
<DialogTitle className="text-hero-darkest">
<div>Turn on 2FA.</div>
<div>Protect Your Assets.</div>
</DialogTitle>
</DialogHeader>
<DialogDescription className="text-center text-steel-darker">
Your {providerToName[activeAccount.provider]} Account now gives access to your Sui
Wallet. To help safeguard your assets, we strongly recommend you enable 2FA.
{providerData.mfaLink ? (
<>
{' '}
<span className="inline-block">
<Link color="heroDark" href={providerData.mfaLink} text="Visit this link" />
</span>{' '}
to find out how to set this up.
</>
) : null}
</DialogDescription>
<DialogFooter>
<Button
text="I understand"
loading={warningMutation.isLoading}
onClick={() =>
warningMutation.mutate(
{ accountID: activeAccount.id },
{
onError: (e) => {
toast.error((e as Error)?.message || 'Something went wrong');
},
},
)
}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return null;
}
7 changes: 4 additions & 3 deletions apps/wallet/src/ui/app/shared/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ DialogOverlay.displayName = RadixDialog.Overlay.displayName;

const DialogContent = React.forwardRef<
React.ElementRef<typeof RadixDialog.Content>,
React.ComponentPropsWithoutRef<typeof RadixDialog.Content>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof RadixDialog.Content> & { background?: 'white' | 'avocado' }
>(({ className, background = 'white', ...props }, ref) => (
<RadixDialog.Portal>
<DialogOverlay />
<RadixDialog.Content
ref={ref}
className={cx(
'fixed flex flex-col justify-center z-[99999] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-wallet-modal bg-white p-6 rounded-xl w-80 max-w-[85vw] max-h-[60vh] overflow-hidden gap-3 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
'fixed flex flex-col justify-center z-[99999] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 shadow-wallet-modal p-6 rounded-xl w-80 max-w-[85vw] max-h-[60vh] overflow-hidden gap-3 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
className,
background === 'white' ? 'bg-white' : 'bg-avocado-200 border border-solid border-hero/10',
)}
{...props}
/>
Expand Down
2 changes: 2 additions & 0 deletions apps/wallet/src/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import App from './app';
import { walletApiProvider } from './app/ApiProvider';
import { AccountsFormProvider } from './app/components/accounts/AccountsFormContext';
import { UnlockAccountProvider } from './app/components/accounts/UnlockAccountContext';
import { ZkLoginAccountWarningModal } from './app/components/accounts/ZkLoginAccountWaringModal';
import { SuiLedgerClientProvider } from './app/components/ledger/SuiLedgerClientProvider';
import { growthbook } from './app/experimentation/feature-gating';
import { persister, queryClient } from './app/helpers/queryClient';
Expand Down Expand Up @@ -93,6 +94,7 @@ function AppWrapper() {
>
<ErrorBoundary>
<App />
<ZkLoginAccountWarningModal />
</ErrorBoundary>
<div id="overlay-portal-container"></div>
<div id="toaster-portal-container"></div>
Expand Down

0 comments on commit 5130ade

Please sign in to comment.