Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LW-9813] Create posthog alias after new wallet is restored #906

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ export const userIdServiceProperties: RemoteApiProperties<UserIdServiceInterface
getUserId: RemoteApiPropertyType.MethodReturningPromise,
userId$: RemoteApiPropertyType.HotObservable,
isNewSession: RemoteApiPropertyType.MethodReturningPromise,
resetToDefaultValues: RemoteApiPropertyType.MethodReturningPromise
resetToDefaultValues: RemoteApiPropertyType.MethodReturningPromise,
generateWalletBasedUserId: RemoteApiPropertyType.MethodReturningPromise
};
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class UserIdService implements UserIdServiceInterface {
this.sessionTimeout = undefined;
}

private generateWalletBasedUserId(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex) {
generateWalletBasedUserId(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex): string {
console.debug('[ANALYTICS] Wallet based ID not found - generating new one');
// by requirement, we want to hash the extended account public key twice
const hash = hashExtendedAccountPublicKey(extendedAccountPublicKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export interface UserIdService {
extendLifespan(): Promise<void>;
resetToDefaultValues(): Promise<void>;
isNewSession(): Promise<boolean>;
generateWalletBasedUserId(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ export class AnalyticsTracker implements IAnalyticsTracker {
await this.postHogClient?.sendAliasEvent();
}

async sendMergeEvent(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex): Promise<void> {
const shouldOmitEvent = this.shouldOmitSendEventToPostHog();
if (shouldOmitEvent) return;
await this.userIdService?.extendLifespan();
await this.checkNewSessionStarted();
await this.postHogClient?.sendMergeEvent(extendedAccountPublicKey);
}

async sendEventToPostHog(action: PostHogAction, properties: PostHogProperties = {}): Promise<void> {
const isEventExcluded = this.isEventExcluded(action);
const shouldOmitEvent = this.shouldOmitSendEventToPostHog();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ export class PostHogClient {
posthog.alias(alias, id);
}

// $merge_dangerously is needed to ensure merge works on users with merge restrictions
// https://posthog.com/docs/product-analytics/identify
async sendMergeEvent(extendedAccountPublicKey: Wallet.Crypto.Bip32PublicKeyHex): Promise<void> {
const id = await this.userIdService.generateWalletBasedUserId(extendedAccountPublicKey);
if (!id) {
console.debug('[ANALYTICS] Wallet-based ID not found');
return;
}

console.debug('[ANALYTICS] Merging wallet-based ID into current user');
posthog.capture('$merge_dangerously', {
alias: id
});
}

async sendEvent(action: PostHogAction, properties: PostHogProperties = {}): Promise<void> {
const payload = {
...(await this.getEventMetadata()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,8 @@ export const userIdServiceMock: Record<keyof UserIdService, jest.Mock> = {
getAliasProperties: jest.fn(),
userId$: new Subject() as any,
isNewSession: jest.fn(() => true),
resetToDefaultValues: jest.fn()
resetToDefaultValues: jest.fn(),
generateWalletBasedUserId: jest.fn()
};

export const postHogClientMocks: Record<keyof typeof PostHogClient.prototype, jest.Mock> = {
Expand All @@ -646,13 +647,15 @@ export const postHogClientMocks: Record<keyof typeof PostHogClient.prototype, je
getExperimentVariant: jest.fn(),
subscribeToDistinctIdUpdate: jest.fn(),
shutdown: jest.fn(),
sendSessionStartEvent: jest.fn()
sendSessionStartEvent: jest.fn(),
sendMergeEvent: jest.fn()
};

export const mockAnalyticsTracker: Record<keyof typeof AnalyticsTracker.prototype, jest.Mock> = {
sendEventToPostHog: jest.fn(),
setOptedInForEnhancedAnalytics: jest.fn(),
sendPageNavigationEvent: jest.fn(),
setChain: jest.fn(),
sendAliasEvent: jest.fn()
sendAliasEvent: jest.fn(),
sendMergeEvent: jest.fn()
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Providers } from './hardware-wallet/types';
import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution';
import { useTranslation } from 'react-i18next';
import { WalletConflictError } from '@cardano-sdk/web-extension';
import { useAnalyticsContext } from '@providers';

const { newWallet } = walletRoutePaths;

Expand All @@ -39,6 +40,7 @@ interface ConfirmationDialog {
export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog): JSX.Element => {
const { t } = useTranslation();
const { connectHardwareWallet, createHardwareWallet } = useWalletManager();
const analytics = useAnalyticsContext();
const disconnectHardwareWallet$ = useMemo(() => new Subject<HIDConnectionEvent>(), []);

const hardwareWalletProviders = useMemo(
Expand All @@ -48,12 +50,13 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog):
shouldShowDialog$,
createWallet: async ({ account, connection, model, name }) => {
try {
await createHardwareWallet({
const { source } = await createHardwareWallet({
connectedDevice: model,
deviceConnection: connection,
name,
accountIndex: account
});
await analytics.sendMergeEvent(source.account.extendedAccountPublicKey);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to send the public key in plain text?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not sent as is. Its hashed first with generateWalletBasedUserId.

} catch (error) {
if (error instanceof WalletConflictError) {
toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') });
Expand All @@ -63,7 +66,7 @@ export const SetupHardwareWallet = ({ shouldShowDialog$ }: ConfirmationDialog):
}
}
}),
[connectHardwareWallet, createHardwareWallet, disconnectHardwareWallet$, shouldShowDialog$, t]
[connectHardwareWallet, createHardwareWallet, disconnectHardwareWallet$, shouldShowDialog$, t, analytics]
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,23 @@ import { StoreProvider } from '@src/stores';
import { APP_MODE_BROWSER } from '@src/utils/constants';
import { AppSettingsProvider, DatabaseProvider } from '@providers';
import { UseWalletManager } from '@hooks/useWalletManager';
import { AnalyticsTracker } from '@providers/AnalyticsProvider/analyticsTracker';

jest.mock('@providers/AnalyticsProvider', () => ({
useAnalyticsContext: jest.fn<Pick<AnalyticsTracker, 'sendMergeEvent'>, []>().mockReturnValue({
sendMergeEvent: jest.fn().mockReturnValue('')
})
}));

jest.mock('@hooks/useWalletManager', () => ({
useWalletManager: jest.fn().mockReturnValue({
createWallet: jest.fn().mockResolvedValue(void 0) as UseWalletManager['createWallet']
createWallet: jest.fn().mockResolvedValue({
source: {
account: {
extendedAccountPublicKey: ''
}
}
}) as UseWalletManager['createWallet']
} as UseWalletManager)
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WarningModal } from '@src/views/browser-view/components';
import { useCreateWallet } from '../context';
import { walletRoutePaths } from '@routes';
import { useWalletManager } from '@hooks/useWalletManager';
import { useAnalyticsContext } from '@providers/AnalyticsProvider';

const noop = (): void => void 0;

Expand All @@ -22,6 +23,7 @@ export const NewRecoveryPhrase = (): JSX.Element => {
const { t } = useTranslation();
const { generatedMnemonic, data } = useCreateWallet();
const { createWallet } = useWalletManager();
const analytics = useAnalyticsContext();
const [state, setState] = useState<State>(() => ({
isResetMnemonicModalOpen: false,
resetMnemonicStage: 'writedown'
Expand All @@ -46,10 +48,11 @@ export const NewRecoveryPhrase = (): JSX.Element => {
}, [data]);

const saveWallet = useCallback(async () => {
await createWallet(data);
const { source } = await createWallet(data);
await analytics.sendMergeEvent(source.account.extendedAccountPublicKey);
clearSecrets();
history.push(walletRoutePaths.assets);
}, [data, createWallet, history, clearSecrets]);
}, [data, createWallet, history, clearSecrets, analytics]);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,26 @@ import { StoreProvider } from '@src/stores';
import { APP_MODE_BROWSER } from '@src/utils/constants';
import { AppSettingsProvider, DatabaseProvider } from '@providers';
import { UseWalletManager } from '@hooks/useWalletManager';
import { AnalyticsTracker } from '@providers/AnalyticsProvider/analyticsTracker';

jest.mock('@hooks/useWalletManager', () => ({
useWalletManager: jest.fn().mockReturnValue({
createWallet: jest.fn().mockResolvedValue(void 0) as UseWalletManager['createWallet']
createWallet: jest.fn().mockResolvedValue({
source: {
account: {
extendedAccountPublicKey: ''
}
}
}) as UseWalletManager['createWallet']
} as UseWalletManager)
}));

jest.mock('@providers/AnalyticsProvider', () => ({
useAnalyticsContext: jest.fn<Pick<AnalyticsTracker, 'sendMergeEvent'>, []>().mockReturnValue({
sendMergeEvent: jest.fn().mockReturnValue('')
})
}));

const keepWalletSecureStep = async () => {
const nextButton = getNextButton();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useWalletManager } from '@hooks';
import { toast } from '@lace/common';
import { TOAST_DEFAULT_DURATION } from '@hooks/useActionExecution';
import { WalletConflictError } from '@cardano-sdk/web-extension';
import { useAnalyticsContext } from '@providers/AnalyticsProvider';

const wordList = wordlists.english;

Expand All @@ -19,6 +20,8 @@ export const RestoreRecoveryPhrase = (): JSX.Element => {
const history = useHistory();
const { data, setMnemonic } = useRestoreWallet();
const { createWallet } = useWalletManager();
const analytics = useAnalyticsContext();

const isValidMnemonic = useMemo(
() => Wallet.KeyManagement.util.validateMnemonic(Wallet.KeyManagement.util.joinMnemonicWords(data.mnemonic)),
[data.mnemonic]
Expand All @@ -42,21 +45,24 @@ export const RestoreRecoveryPhrase = (): JSX.Element => {
passphraseError: t('core.walletSetupMnemonicStep.passphraseError')
};

const onSubmitForm = useCallback(async () => {
event.preventDefault();

try {
await createWallet(data);
} catch (error) {
if (error instanceof WalletConflictError) {
toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') });
} else {
throw error;
const onSubmitForm = useCallback(
async (event: Readonly<React.MouseEvent<HTMLButtonElement>>) => {
event.preventDefault();
try {
const { source } = await createWallet(data);
await analytics.sendMergeEvent(source.account.extendedAccountPublicKey);
} catch (error) {
if (error instanceof WalletConflictError) {
toast.notify({ duration: TOAST_DEFAULT_DURATION, text: t('multiWallet.walletAlreadyExists') });
} else {
throw error;
}
}
}
clearSecrets();
history.push(walletRoutePaths.assets);
}, [data, clearSecrets, createWallet, history, t]);
clearSecrets();
history.push(walletRoutePaths.assets);
},
[data, clearSecrets, createWallet, history, t, analytics]
);

return (
<WalletSetupMnemonicVerificationStep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ export const HardwareWalletFlow = ({
if (isAnalyticsAccepted) {
await analytics.sendAliasEvent();
}
// Workaround to enable staking with Ledger right after the onboarding LW-5564
window.location.reload();

if (typeof deviceConnection === 'object') {
VanessaPC marked this conversation as resolved.
Show resolved Hide resolved
deviceConnection.transport.close();
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface WalletSetupMnemonicVerificationStepProps {
mnemonic: string[];
onChange: (words: string[]) => void;
onCancel: () => void;
onSubmit: () => void;
onSubmit: (event: Readonly<React.MouseEvent<HTMLButtonElement>>) => void;
onStepNext?: (currentStep: number) => void;
isSubmitEnabled: boolean;
mnemonicWordsInStep?: number;
Expand Down Expand Up @@ -43,14 +43,14 @@ export const WalletSetupMnemonicVerificationStep = ({
onCancel();
};

const handleNext = () => {
const handleNext = (event: Readonly<React.MouseEvent<HTMLButtonElement>>) => {
if (onStepNext) onStepNext(mnemonicStep);
if (mnemonicStep < mnemonicSteps - 1) {
setMnemonicStep(mnemonicStep + 1);
return;
}

onSubmit();
onSubmit(event);
};

const getStepInfoText = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface WalletSetupStepLayoutProps {
description?: React.ReactNode;
linkText?: React.ReactNode;
stepInfoText?: string;
onNext?: () => void;
onNext?: (event: Readonly<React.MouseEvent<HTMLButtonElement>>) => void;
onBack?: () => void;
onSkip?: () => void;
nextLabel?: string;
Expand Down
Loading