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

Get referral code from the web app when onboarding invitee #618

Merged
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
10 changes: 10 additions & 0 deletions src/background/Wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import { getDefiSdkClient } from 'src/modules/defi-sdk/background';
import type { NetworkConfig } from 'src/modules/networks/NetworkConfig';
import type { LocallyEncoded } from 'src/shared/wallet/encode-locally';
import { decodeMasked } from 'src/shared/wallet/encode-locally';
import type { RemoteConfig } from 'src/modules/remote-config';
import { getRemoteConfigValue } from 'src/modules/remote-config';
import { ZerionAPI } from 'src/modules/zerion-api/zerion-api.background';
import type { DaylightEventParams, ScreenViewParams } from '../events';
import { emitter } from '../events';
Expand Down Expand Up @@ -854,6 +856,14 @@ export class Wallet {
return globalPreferences.setPreferences(preferences);
}

async getRemoteConfigValue({
context,
params: { key },
}: WalletMethodParams<{ key: keyof RemoteConfig }>) {
this.verifyInternalOrigin(context);
return getRemoteConfigValue(key);
}

async getFeedInfo({
context,
}: WalletMethodParams): Promise<ReturnType<typeof Model.getFeedInfo>> {
Expand Down
20 changes: 2 additions & 18 deletions src/modules/remote-config/plugins/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import throttle from 'lodash/throttle';
import { useQuery } from '@tanstack/react-query';
import ky from 'ky';
import { PROXY_URL } from 'src/env/config';
import type { ConfigPlugin } from '../ConfigPlugin';
Expand All @@ -9,11 +8,13 @@ import type { RemoteConfig } from '../types';
const defaultConfig: RemoteConfig = {
extension_wallet_name_flags: {},
extension_uninstall_link: '',
extension_referral_program: false,
};

const knownKeys: (keyof RemoteConfig)[] = [
'extension_wallet_name_flags',
'extension_uninstall_link',
'extension_referral_program',
];

async function fetchRemoteConfig<T extends keyof RemoteConfig>(keys: T[]) {
Expand Down Expand Up @@ -62,20 +63,3 @@ export const firebase: ConfigPlugin & { refresh(): void } = {
return { value };
},
};

export function useFirebaseConfig<T extends keyof RemoteConfig>(
keys: T[],
{ suspense = false }: { suspense?: boolean } = {}
) {
return useQuery({
// it's okay to put the `keys` array inside queryKey array without memoizing:
// it will be stringified anyway
// https://github.com/TanStack/query/blob/b18426da86e2b8990e8f4e7398baaf041f77ad19/packages/query-core/src/utils.ts#L269-L280
queryKey: ['fetchRemoteConfig', keys],
queryFn: () => fetchRemoteConfig(keys),
retry: 0,
refetchOnWindowFocus: false,
staleTime: 20000,
suspense,
});
}
1 change: 1 addition & 0 deletions src/modules/remote-config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import type { WalletNameFlag } from 'src/shared/types/WalletNameFlag';
export interface RemoteConfig {
extension_wallet_name_flags: Record<string, WalletNameFlag[]>;
extension_uninstall_link: string;
extension_referral_program: boolean;
}
18 changes: 18 additions & 0 deletions src/modules/remote-config/useRemoteConfigValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useQuery } from '@tanstack/react-query';
import { walletPort } from 'src/ui/shared/channels';
import type { RemoteConfig } from './types';

export function useRemoteConfigValue<K extends keyof RemoteConfig>(key: K) {
return useQuery({
queryKey: ['wallet/getRemoteConfigValue', key],
queryFn: async () => {
const value = await walletPort.request('getRemoteConfigValue', { key });
return value as RemoteConfig[K];
},
useErrorBoundary: false,
staleTime: 10000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to provide suspense here?
Hope this will not lead to ui blinking

Copy link
Contributor Author

@vyorkin vyorkin Oct 11, 2024

Choose a reason for hiding this comment

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

✅ Yep, let's have suspense: false

suspense: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
}
18 changes: 12 additions & 6 deletions src/modules/zerion-api/requests/check-referral.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import type { ClientOptions } from '../shared';
import { ZerionHttpClient } from '../shared';

interface Params {
referralCode: string;
}

export interface ReferrerData {
referralCode: string;
address: string | null;
handle: string | null;
}

interface Response {
data: null;
data: ReferrerData;
errors?: { title: string; detail: string }[];
}

export function checkReferral(params: Params) {
return ZerionHttpClient.post<Response>({
endpoint: 'wallet/check-referral/v1',
body: JSON.stringify({ referralCode: params.referralCode }),
});
export function checkReferral(payload: Params, options?: ClientOptions) {
const params = new URLSearchParams({ referralCode: payload.referralCode });
const endpoint = `wallet/check-referral/v1?${params}`;
return ZerionHttpClient.get<Response>({ endpoint, ...options });
}
16 changes: 16 additions & 0 deletions src/ui/features/onboarding/Success/Success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import JigsawIcon from 'jsx:./assets/jigsaw.svg';
import coinImgSrc from 'src/ui/assets/zer_coin.png';
import sparkImgSrc from 'src/ui/assets/zer_spark.png';
import starImgSrc from 'src/ui/assets/zer_star.png';
import { useRemoteConfigValue } from 'src/modules/remote-config/useRemoteConfigValue';
import * as styles from './styles.module.css';
import { WebAppMessageHandler } from './WebAppMessageHandler';

const DEBUG_INVITEE_FLOW = false;

export function Success() {
const { isNarrowView } = useWindowSizeStore();
Expand Down Expand Up @@ -107,8 +111,20 @@ export function Success() {
},
});

const {
data: referralProgramEnabled,
isLoading: isLoadingRemoteConfigValue,
} = useRemoteConfigValue('extension_referral_program');

if (isLoadingRemoteConfigValue) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we use isLoading, we can set suspense: false inside useQuery

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated ✅

return null;
}

return (
<>
{referralProgramEnabled || DEBUG_INVITEE_FLOW ? (
<WebAppMessageHandler pathname="/get-referral-code" />
) : null}
<canvas
ref={confettiRef}
style={{
Expand Down
81 changes: 81 additions & 0 deletions src/ui/features/onboarding/Success/WebAppMessageHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useEffect, useRef } from 'react';
import { ZerionAPI } from 'src/modules/zerion-api/zerion-api.client';
import { invariant } from 'src/shared/invariant';
import { isObj } from 'src/shared/isObj';

const ZERION_WEB_APP_URL = new URL('https://app.zerion.io');

type WebAppCallbackMethod = 'set-referral-code';

interface WebAppMessage {
method: WebAppCallbackMethod;
params?: unknown;
}

export function isWebAppMessage(
event: MessageEvent,
{
expectedSource,
}: {
expectedSource: Window | null;
}
): event is MessageEvent<WebAppMessage> {
return (
event.origin === ZERION_WEB_APP_URL.origin &&
event.source === expectedSource &&
isObj(event.data) &&
'method' in event.data
);
}

async function setReferralCode(referralCode: string) {
const response = await ZerionAPI.checkReferral({ referralCode });
// @ts-ignore
const checkedReferrer = response.data;
// await saveReferrer(checkedReferrer);
}

async function handleMessage({
event,
expectedSource,
}: {
event: MessageEvent;
expectedSource: Window | null;
}) {
if (!isWebAppMessage(event, { expectedSource })) return;
const { method, params } = event.data;

if (method === 'set-referral-code') {
invariant(
isObj(params) && typeof params.referralCode === 'string',
'Invalid payload for set-referral-code web app message'
);

await setReferralCode(params.referralCode);
}
}

export function WebAppMessageHandler({ pathname }: { pathname: string }) {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const iframeUrl = new URL(pathname, ZERION_WEB_APP_URL);

useEffect(() => {
invariant(iframeRef.current, 'Iframe should be mounted');
const webAppWindow = iframeRef.current.contentWindow;

const handler = (event: MessageEvent) =>
handleMessage({ event, expectedSource: webAppWindow });

window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);

return (
<iframe
sandbox="allow-same-origin allow-scripts"
ref={iframeRef}
src={iframeUrl.toString()}
hidden={true}
/>
);
}
2 changes: 1 addition & 1 deletion src/ui/hardware-wallet/LedgerIframe/LedgerIframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const LedgerIframe = React.forwardRef(function LedgerIframeComponent(
id="iframe-component"
{...props}
// This is crucial: by lifting only "allow-scripts" restriction
// we restrict everything else, inluding "allow-same-origin" token.
// we restrict everything else, including "allow-same-origin" token.
// By doing this, the iframe code will be treated by the background script
// as a third-party origin.
sandbox="allow-scripts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function isAllowedMessage(
) {
// NOTE:
// Checking the origin of a sandboxed iframe:
// https://web.dev/sandboxed-iframes/#safely-sandboxing-eval
// https://web.dev/sandboxed-iframes/#safely_sandboxing_eval
return (
event.origin === 'null' &&
event.source === targetIframeElement.contentWindow
Expand Down
Loading