Skip to content

Commit

Permalink
feat: paper wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
mchappell committed Aug 14, 2024
1 parent ee97a86 commit 22fa7d6
Show file tree
Hide file tree
Showing 96 changed files with 3,914 additions and 389 deletions.
10 changes: 10 additions & 0 deletions apps/browser-extension-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@lace/core": "0.1.0",
"@lace/staking": "0.1.0",
"@lace/translation": "0.1.0",
"@pdfme/generator": "^4.0.2",
"@react-rxjs/core": "^0.9.8",
"@react-rxjs/utils": "^0.9.5",
"@vespaiach/axios-fetch-adapter": "^0.3.0",
Expand All @@ -73,12 +74,15 @@
"graphql-tag": "2.12.5",
"i18next": "^22.5.1",
"intersection-observer-polyfill": "0.1.0",
"jsqr": "^1.4.0",
"lodash": "4.17.21",
"node-abort-controller": "^3.1.1",
"openpgp": "^5.11.2",
"p-debounce": "^4.0.0",
"pluralize": "^8.0.0",
"posthog-js": "^1.68.4",
"process": "^0.11.10",
"qrcode": "^1.5.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "^12.3.1",
Expand All @@ -93,9 +97,14 @@
"devDependencies": {
"@cardano-sdk/hardware-ledger": "0.11.0",
"@emurgo/cardano-message-signing-asmjs": "1.0.1",
"@openpgp/web-stream-tools": "0.0.11-patch-0",
"@pdfme/common": "^4.0.2",
"@pdfme/schemas": "^4.0.2",
"@types/dotenv-webpack": "7.0.3",
"@types/pluralize": "^0.0.29",
"@types/qrcode": "^1",
"@types/react-lottie": "^1.2.6",
"@types/text-encoding-utf-8": "^1",
"@types/uuid": "^8.3.4",
"@types/w3c-web-hid": "^1.0.3",
"@types/webextension-polyfill": "0.8.0",
Expand All @@ -105,6 +114,7 @@
"fake-indexeddb": "3.1.3",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"jest-webextension-mock": "^3.7.19",
"text-encoding-utf-8": "^1.0.2",
"tsconfig-paths-webpack-plugin": "3.5.2",
"webassembly-loader-sw": "^1.1.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AuthorizedDappStorage } from '@src/types/dappConnector';
import type { Message } from './background-service';
import { ADAPrices } from './prices';
import { ExperimentName } from '@providers/ExperimentsProvider/types';

export interface PendingMigrationState {
from: string;
Expand Down Expand Up @@ -28,7 +29,7 @@ export interface BackgroundStorage {
fiatPrices?: { prices: ADAPrices; timestamp: number };
userId?: string;
usePersistentUserId?: boolean;
experimentsConfiguration?: Record<string, string | boolean>;
experimentsConfiguration?: Record<ExperimentName, string | boolean>;
customSubmitTxUrl?: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,25 @@ const makeMultiWalletRestoreEvent = <E extends string>(eventSuffix: E) =>
`multiwallet | restore wallet revamp | ${eventSuffix}` as const;
const makeMultiWalletHardwareEvent = <E extends string>(eventSuffix: E) =>
`multiwallet | hardware wallet revamp | ${eventSuffix}` as const;
const makePaperWalletOnboardingCreateEvent = <E extends string>(eventSuffix: E) =>
`onboarding | new wallet revamp paper wallet | ${eventSuffix}` as const;
const makePaperWalletOnboardingRestoreEvent = <E extends string>(eventSuffix: E) =>
`onboarding | restore wallet revamp paper wallet | ${eventSuffix}` as const;

const multiWalletActions = {
create: {
CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | recovery phrase | click'),
CHOOSE_RECOVERY_MODE_PAPER_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | paper wallet | click'),
CHOOSE_RECOVERY_MODE_NEXT_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | next | click'),
PGP_PUBLIC_KEY_PAGE_VIEW: makePaperWalletOnboardingCreateEvent('step: pgp key | pageview'),
PGP_PUBLIC_KEY_NEXT_CLICK: makePaperWalletOnboardingCreateEvent('step: pgp key | next | click'),
WALLET_SETUP_GENERATE_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent(
'step: wallet info | Generate paper wallet | click'
),
PAPER_WALLET_DOWNLOAD_PAGEVIEW: makePaperWalletOnboardingCreateEvent('step: download pdf | pageview'),
DOWNLOAD_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent('download pdf | download pdf | click'),
PRINT_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent('print pdf | print pdf | click'),
PAPER_WALLET_COMPLETE_CLICK: makePaperWalletOnboardingCreateEvent('open wallet | open wallet | click'),
SETUP_OPTION_CLICK: makeMultiWalletCreateEvent('create | click'),
SAVE_RECOVERY_PHRASE_NEXT_CLICK: makeMultiWalletCreateEvent('save your recovery phrase | next | click'),
ENTER_RECOVERY_PHRASE_NEXT_CLICK: makeMultiWalletCreateEvent('enter your recovery phrase | next | click'),
Expand All @@ -35,6 +51,18 @@ const multiWalletActions = {
WALLET_ADDED: makeMultiWalletCreateEvent('added')
},
restore: {
WALLET_SETUP_PAGEVIEW: makePaperWalletOnboardingCreateEvent('step: wallet info | pageview'),
CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | recovery phrase | click'),
CHOOSE_RECOVERY_MODE_PAPER_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | paper wallet | click'),
CHOOSE_RECOVERY_MODE_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | next | click'),
SCAN_QR_CODE_PAGEVIEW: makePaperWalletOnboardingRestoreEvent('step: scan qr code | pageview'),
SCAN_QR_CODE_CAMERA_OK: makePaperWalletOnboardingRestoreEvent('step: scan qr code | camera ok'),
SCAN_QR_CODE_CAMERA_ERROR: makePaperWalletOnboardingRestoreEvent('step: scan qr code | camera error'),
SCAN_QR_CODE_READ_SUCCESS: makePaperWalletOnboardingRestoreEvent('step: scan qr code | read success'),
SCAN_QR_CODE_READ_ERROR: makePaperWalletOnboardingRestoreEvent('step: scan qr code | read error'),
WALLET_OVERVIEW_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('step: wallet overview | next | click'),
ENTER_PGP_PRIVATE_KEY_PAGE_VIEW: makePaperWalletOnboardingRestoreEvent('step: pgp private key | pageview'),
ENTER_PGP_PRIVATE_KEY_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('step: pgp private key | next | click'),
SETUP_OPTION_CLICK: makeMultiWalletRestoreEvent('restore | click'),
ENTER_WALLET: makeMultiWalletRestoreEvent("let's set up your new wallet | enter wallet | click"),
ENTER_RECOVERY_PHRASE_NEXT_CLICK: makeMultiWalletRestoreEvent(' enter your recovery phrase | next | click'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const makeOnboardingHardwareEvent = <E extends string>(eventSuffix: E) =>
`onboarding | hardware wallet revamp | ${eventSuffix}` as const;
const makeForgotPasswordEvent = <E extends string>(eventSuffix: E) =>
`unlock wallet | forgot password? | ${eventSuffix}` as const;
const makePaperWalletOnboardingCreateEvent = <E extends string>(eventSuffix: E) =>
`onboarding | new wallet revamp paper wallet | ${eventSuffix}` as const;
const makePaperWalletOnboardingRestoreEvent = <E extends string>(eventSuffix: E) =>
`onboarding | restore wallet revamp paper wallet | ${eventSuffix}` as const;

const onboardingActions = {
onboarding: {
Expand All @@ -23,6 +27,18 @@ const onboardingActions = {
PIN_EXTENSION_CLICK: makeOnboardingEvent('lace main view | pin the wallet extension | click')
},
create: {
CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | recovery phrase | click'),
CHOOSE_RECOVERY_MODE_PAPER_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | paper wallet | click'),
CHOOSE_RECOVERY_MODE_NEXT_CLICK: makePaperWalletOnboardingCreateEvent('choose mode | next | click'),
PGP_PUBLIC_KEY_PAGE_VIEW: makePaperWalletOnboardingCreateEvent('step: pgp key | pageview'),
PGP_PUBLIC_KEY_NEXT_CLICK: makePaperWalletOnboardingCreateEvent('step: pgp key | next | click'),
WALLET_SETUP_GENERATE_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent(
'step: wallet info | Generate paper wallet | click'
),
PAPER_WALLET_DOWNLOAD_PAGEVIEW: makePaperWalletOnboardingCreateEvent('step: download pdf | pageview'),
DOWNLOAD_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent('download pdf | download pdf | click'),
PRINT_PAPER_WALLET_CLICK: makePaperWalletOnboardingCreateEvent('print pdf | print pdf | click'),
PAPER_WALLET_COMPLETE_CLICK: makePaperWalletOnboardingCreateEvent('open wallet | open wallet | click'),
SETUP_OPTION_CLICK: makeOnboardingCreateEvent('create | click'),
SAVE_RECOVERY_PHRASE_NEXT_CLICK: makeOnboardingCreateEvent('save your recovery phrase | next | click'),
ENTER_RECOVERY_PHRASE_NEXT_CLICK: makeOnboardingCreateEvent('enter your recovery phrase | next | click'),
Expand All @@ -46,6 +62,18 @@ const onboardingActions = {
WALLET_ADDED: makeOnboardingCreateEvent('added')
},
restore: {
WALLET_SETUP_PAGEVIEW: makePaperWalletOnboardingCreateEvent('step: wallet info | pageview'),
CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | recovery phrase | click'),
CHOOSE_RECOVERY_MODE_PAPER_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | paper wallet | click'),
CHOOSE_RECOVERY_MODE_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('choose mode | next | click'),
SCAN_QR_CODE_PAGEVIEW: makePaperWalletOnboardingRestoreEvent('step: scan qr code | pageview'),
SCAN_QR_CODE_CAMERA_OK: makePaperWalletOnboardingRestoreEvent('step: scan qr code | camera ok'),
SCAN_QR_CODE_CAMERA_ERROR: makePaperWalletOnboardingRestoreEvent('step: scan qr code | camera error'),
SCAN_QR_CODE_READ_SUCCESS: makePaperWalletOnboardingRestoreEvent('step: scan qr code | read success'),
SCAN_QR_CODE_READ_ERROR: makePaperWalletOnboardingRestoreEvent('step: scan qr code | read error'),
WALLET_OVERVIEW_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('step: wallet overview | next | click'),
ENTER_PGP_PRIVATE_KEY_PAGE_VIEW: makePaperWalletOnboardingRestoreEvent('step: pgp private key | pageview'),
ENTER_PGP_PRIVATE_KEY_NEXT_CLICK: makePaperWalletOnboardingRestoreEvent('step: pgp private key | next | click'),
SETUP_OPTION_CLICK: makeOnboardingRestoreEvent('restore | click'),
ENTER_WALLET: makeOnboardingRestoreEvent("let's set up your new wallet | enter wallet | click"),
ENTER_RECOVERY_PHRASE_NEXT_CLICK: makeOnboardingRestoreEvent(' enter your recovery phrase | next | click'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,32 @@ export type CreateFlowActions = Record<
| 'RECOVERY_PHRASE_PASTE_FROM_CLIPBOARD_CLICK'
| 'RECOVERY_PHRASE_COPY_READ_MORE_CLICK'
| 'RECOVERY_PHRASE_PASTE_READ_MORE_CLICK'
| 'WALLET_ADDED',
| 'WALLET_ADDED'
| 'CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK'
| 'CHOOSE_RECOVERY_MODE_PAPER_CLICK'
| 'CHOOSE_RECOVERY_MODE_NEXT_CLICK'
| 'PGP_PUBLIC_KEY_PAGE_VIEW'
| 'PGP_PUBLIC_KEY_NEXT_CLICK'
| 'WALLET_SETUP_GENERATE_PAPER_WALLET_CLICK'
| 'PAPER_WALLET_DOWNLOAD_PAGEVIEW'
| 'DOWNLOAD_PAPER_WALLET_CLICK'
| 'PRINT_PAPER_WALLET_CLICK'
| 'PAPER_WALLET_COMPLETE_CLICK',
string
>;
export type RestoreFlowActions = Record<
| 'WALLET_SETUP_PAGEVIEW'
| 'CHOOSE_RECOVERY_MODE_MNEMONIC_CLICK'
| 'CHOOSE_RECOVERY_MODE_PAPER_CLICK'
| 'CHOOSE_RECOVERY_MODE_NEXT_CLICK'
| 'SCAN_QR_CODE_PAGEVIEW'
| 'SCAN_QR_CODE_CAMERA_OK'
| 'SCAN_QR_CODE_CAMERA_ERROR'
| 'SCAN_QR_CODE_READ_SUCCESS'
| 'SCAN_QR_CODE_READ_ERROR'
| 'WALLET_OVERVIEW_NEXT_CLICK'
| 'ENTER_PGP_PRIVATE_KEY_PAGE_VIEW'
| 'ENTER_PGP_PRIVATE_KEY_NEXT_CLICK'
| 'SETUP_OPTION_CLICK'
| 'ENTER_RECOVERY_PHRASE_NEXT_CLICK'
| 'ENTER_WALLET'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { ExperimentName, ExperimentsConfig, FallbackConfiguration } from './types';

export const fallbackConfiguration: FallbackConfiguration = {
[ExperimentName.COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN]: 'control'
[ExperimentName.COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN]: 'control',
[ExperimentName.CREATE_PAPER_WALLET]: false,
[ExperimentName.RESTORE_PAPER_WALLET]: false
};

export const experiments: ExperimentsConfig = {
[ExperimentName.COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN]: {
variants: ['control', 'test'],
defaultVariant: 'control'
default: 'control'
},
[ExperimentName.CREATE_PAPER_WALLET]: {
value: false,
default: false
},
[ExperimentName.RESTORE_PAPER_WALLET]: {
value: false,
default: false
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { usePostHogClientContext } from '@providers/PostHogClientProvider';
import { ExperimentName, ExperimentsConfigStatus } from './types';

Expand All @@ -15,6 +15,12 @@ interface ExperimentsProviderProps {
children: React.ReactNode;
}

export const useExperimentsContext = (): ExperimentsContext => {
const experiementsContext = useContext(ExperimentsContext);
if (experiementsContext === null) throw new Error('ExperimentsContext not defined');
return experiementsContext;
};

export const ExperimentsProvider = ({ children }: ExperimentsProviderProps): React.ReactElement => {
const postHogClient = usePostHogClientContext();
const [experimentsConfigStatus, setExperimentsConfigStatus] = useState(ExperimentsConfigStatus.IDLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,26 @@ export enum ExperimentsConfigStatus {
}

export enum ExperimentName {
COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN = 'combined-setup-name-password'
COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN = 'combined-setup-name-password',
CREATE_PAPER_WALLET = 'create-paper-wallet',
RESTORE_PAPER_WALLET = 'restore-paper-wallet'
}

interface FeatureFlag {
value: boolean;
default: boolean;
}

type Variant = ReadonlyArray<string>;

interface ExperiementVariant {
variants: Variant;
default: string;
}

export type CombinedSetupNamePasswordVariants = readonly ['control', 'test'];

export type ExperimentsConfig = {
[ExperimentName.COMBINED_NAME_PASSWORD_ONBOARDING_SCREEN]: {
variants: CombinedSetupNamePasswordVariants;
defaultVariant: string;
};
[key in ExperimentName]: FeatureFlag | ExperiementVariant;
};
export type FallbackConfiguration = Record<ExperimentName, 'control'>;
export type FallbackConfiguration = Record<ExperimentName, 'control' | boolean>;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { ExperimentName } from '@providers/ExperimentsProvider/types';
import { Subscription, BehaviorSubject } from 'rxjs';
import { PostHogAction, PostHogProperties } from '@lace/common';

type FeatureFlags = 'create-paper-wallet' | 'restore-paper-wallet';

/**
* PostHog API reference:
* https://posthog.com/docs/libraries/js
Expand All @@ -34,7 +36,9 @@ export class PostHogClient<Action extends string = string> {
private hasPostHogInitialized$: BehaviorSubject<boolean>;
private subscription: Subscription;
private initSuccess: Promise<boolean>;

featureFlags: {
[key in FeatureFlags]: string | boolean;
};
constructor(
private chain: Wallet.Cardano.ChainId,
private userIdService: UserIdService,
Expand Down Expand Up @@ -82,6 +86,7 @@ export class PostHogClient<Action extends string = string> {
});

this.subscribeToDistinctIdUpdate();
this.loadExperiments();
}

static getInstance(
Expand Down Expand Up @@ -118,10 +123,6 @@ export class PostHogClient<Action extends string = string> {
posthog.register({
distinct_id: id
});

if (type === UserTrackingType.Enhanced && !this.hasPostHogInitialized$.value) {
this.loadExperiments();
}
}
});
}
Expand Down Expand Up @@ -194,27 +195,38 @@ export class PostHogClient<Action extends string = string> {
posthog.featureFlags.override(flags);
}

async getExperimentVariant(key: ExperimentName): Promise<string> {
async getExperimentVariant(key: ExperimentName): Promise<string | boolean> {
const variant = posthog?.getFeatureFlag(key, {
send_event: false
});
// if we get a type of boolean means that the experiment is not running, so we return the fallback variant
if (typeof variant === 'boolean') {
return experiments[key].defaultVariant;
return experiments[key].default;
}

// if the variant does not exist, we need to check for out cache
if (!variant) {
const backgroundStorage = await this.backgroundServiceUtils.getBackgroundStorage();
return (backgroundStorage?.experimentsConfiguration?.[key] as string) || experiments[key].defaultVariant;
return (backgroundStorage?.experimentsConfiguration?.[key] as string) || experiments[key].default;
}

return variant;
}

isFeatureEnabled(key: ExperimentName | string): boolean {
try {
const isEnabled = posthog.isFeatureEnabled(key);
return isEnabled || false;
} catch {
return false;
}
}

protected loadExperiments(): void {
posthog.onFeatureFlags(async () => {
const postHogExperimentConfiguration = posthog.featureFlags.getFlagVariants();
const postHogExperimentConfiguration: Record<ExperimentName, string | boolean> =
posthog.featureFlags.getFlagVariants();
this.featureFlags = postHogExperimentConfiguration;
const backgroundStorage = await this.backgroundServiceUtils.getBackgroundStorage();
if (!backgroundStorage?.experimentsConfiguration && postHogExperimentConfiguration) {
// save current posthog config in background storage
Expand Down
2 changes: 2 additions & 0 deletions apps/browser-extension-wallet/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './activity-detail';
export * from './dappConnector';
export * from './ui';
export * from './side-menu';
export * from './pgp';
export * from './paperWallet';
6 changes: 6 additions & 0 deletions apps/browser-extension-wallet/src/types/paperWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface PaperWalletPDF {
blob?: Blob;
url?: string;
error?: Error;
loading: boolean;
}
13 changes: 13 additions & 0 deletions apps/browser-extension-wallet/src/types/pgp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Message } from 'openpgp';

export interface PublicPgpKeyData {
pgpPublicKey: string;
pgpKeyReference?: string;
}

export interface ShieldedPgpKeyData {
pgpPrivateKey: string;
pgpKeyPassphrase?: string;
shieldedMessage: Message<Uint8Array>;
privateKeyIsDecrypted: boolean;
}
Loading

0 comments on commit 22fa7d6

Please sign in to comment.