Skip to content

Commit

Permalink
Update the attribution property (#1420)
Browse files Browse the repository at this point in the history
* Update the attribution property

* fix config page in sdk playground

* add validation helper
  • Loading branch information
cb-jake authored Oct 7, 2024
1 parent 960e67e commit 3da9ef6
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 177 deletions.
226 changes: 86 additions & 140 deletions examples/testapp/src/components/SDKConfig/SDKConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,29 @@ import {
FormHelperText,
Heading,
Input,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
VStack,
Divider,
Switch,
} from "@chakra-ui/react";
import React, { useCallback, useMemo, useState } from "react";
import { createCoinbaseWalletSDK } from "@coinbase/wallet-sdk";
import { useCBWSDK } from "../../context/CBWSDKReactContextProvider";
import {
Preference,
} from "@coinbase/wallet-sdk/dist/core/provider/interface";
import { CheckIcon, ChevronDownIcon } from "@chakra-ui/icons";
import { Preference } from "@coinbase/wallet-sdk/dist/core/provider/interface";
import { keccak256, slice, toHex } from "viem";
import { CreateCoinbaseWalletSDKOptions } from "@coinbase/wallet-sdk/dist/createCoinbaseWalletSDK";

type PostOnboardingAction = "none" | "onramp" | "magicspend";

const postOnboardingActions = ["none", "onramp", "magicspend"] as const;

type OnrampPrefillOptions = {
contractAddress?: string;
amount: string;
chainId: number;
};

type Config = {
postOnboardingAction?: PostOnboardingAction;
onrampPrefillOptions?: OnrampPrefillOptions;
attributionDataSuffix?: string;
};
function is0xString(value: string): value is `0x${string}` {
return value.startsWith("0x");
}

export function SDKConfig() {
const { option, scwUrl } = useCBWSDK();
const [config, setConfig] = React.useState<Config>({});
const [config, setConfig] = React.useState<Preference>({
options: option,
attribution: {
auto: true,
},
});

const options: CreateCoinbaseWalletSDKOptions = useMemo(() => {
const preference: Preference = {
Expand All @@ -72,45 +58,44 @@ export function SDKConfig() {
await provider.request({ method: "eth_requestAccounts" });
}, [options]);

const handlePostOnboardingAction = useCallback(
(action: PostOnboardingAction) => {
const config_ = { ...config, postOnboardingAction: action };
if (action !== "onramp") {
delete config_.onrampPrefillOptions;
}
const handleSetAttributionAuto = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const config_: Preference = {
...config,
attribution: {
auto: event.target.checked,
},
};
setConfig(config_);
},
[config]
);

const handleOnrampPrefill = useCallback(
(key: "contractAddress" | "amount" | "chainId") => (e) => {
const value = e.target.value;
setConfig((prev) => ({
...prev,
onrampPrefillOptions: {
...prev.onrampPrefillOptions,
[key]: value,
},
}));
const handleSetDataSuffix = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
if (is0xString(value)) {
setConfig((prev) => ({
...prev,
attribution: {
dataSuffix: value,
},
}));
}
},
[]
);

const handleSetDataSuffix = useCallback((e) => {
const value = e.target.value;
setConfig((prev) => ({
...prev,
attributionDataSuffix: value,
}));
}, []);

const [dataSuffix, setDataSuffix] = useState("Coinbase Wallet");
const fourByteHex = useMemo(
() => slice(keccak256(toHex(dataSuffix)), 0, 4),
[dataSuffix]
);

const attributionAuto = useMemo(() => {
return "auto" in config.attribution && config.attribution?.auto;
}, [config.attribution]);

return (
<Card>
<CardHeader>
Expand All @@ -131,104 +116,65 @@ export function SDKConfig() {
<CardBody>
<Flex justify="space-between" align="center">
<Box>
<Heading size="md">Post Onboarding Action</Heading>
<Code mt={2}>postOnboardingAction</Code>
<Heading size="md">Attribution</Heading>
<Code mt={2}>attribution.auto</Code>
</Box>
<Menu>
<MenuButton
colorScheme="telegram"
as={Button}
rightIcon={<ChevronDownIcon />}
>
{config?.postOnboardingAction}
</MenuButton>
<MenuList>
{postOnboardingActions.map((action) => (
<MenuItem
color={"MenuText"}
key={action}
icon={
action === config.postOnboardingAction ? (
<CheckIcon />
) : null
}
onClick={() => handlePostOnboardingAction(action)}
>
{action}
</MenuItem>
))}
</MenuList>
</Menu>
</Flex>
{config.postOnboardingAction === "onramp" && (
<>
<Divider my={6} />
<Flex justify="space-between" align="center" mt={6}>
<Box>
<Heading size="md">Onramp Prefill Options</Heading>
<Text fontSize="sm" maxW="400px">
Optional: Only works when postOnboardingAction is set to
onramp. Amount and chainId are required. If contract address
is omitted, onramp assumes native asset for that chain
</Text>
<Code mt={2}>onrampPrefillOptions</Code>
</Box>
<VStack>
<Input
placeholder="Contract Address"
onChange={handleOnrampPrefill("contractAddress")}
/>
<Input
placeholder="Amount (wei)"
required
onChange={handleOnrampPrefill("amount")}
/>
<Input
placeholder="Chain ID"
required
onChange={(e) =>
handleOnrampPrefill("chainId")({
target: { value: parseInt(e.target.value, 10) },
})
}
/>
</VStack>
</Flex>
</>
)}
<Divider my={6} />
<Flex justify="space-between" align="center">
<Box>
<Heading size="md">Attribution Data Suffix</Heading>
<Text fontSize="sm">
First 4 bytes of a unique string to identify your onchain activity
</Text>
<FormControl mt={2}>
<FormLabel>
<Code>attributionDataSuffix</Code>
</FormLabel>
<Input
mt={2}
type="text"
placeholder="Enter String"
onChange={(e) => setDataSuffix(e.target.value)}
value={dataSuffix}
<Switch
defaultChecked={attributionAuto}
onChange={handleSetAttributionAuto}
/>
<FormHelperText>
Convert any string into a 4 byte data suffix
</FormHelperText>
</FormControl>
<Code mt={2} colorScheme="telegram">
{fourByteHex}
</Code>
</Box>
<VStack>
<Input
placeholder="Data Suffix (4 bytes)"
onChange={handleSetDataSuffix}
/>
</VStack>
</Flex>
{!attributionAuto && (
<Flex
justify="space-between"
align={{
base: "flex-start",
md: "center",
}}
my={2}
flexDirection={{
base: "column",
md: "row",
}}
>
<Box>
<Heading size="sm">Data Suffix</Heading>
<Text fontSize="sm">
First 4 bytes of a unique string to identify your onchain
activity
</Text>
<FormControl mt={2}>
<FormLabel>
<Code>attribution.dataSuffix</Code>
</FormLabel>
<Input
mt={2}
type="text"
placeholder="Enter String"
onChange={(e) => setDataSuffix(e.target.value)}
value={dataSuffix}
/>
<FormHelperText>
Convert any string into a 4 byte data suffix
</FormHelperText>
</FormControl>
<Code mt={2} colorScheme="telegram">
{fourByteHex}
</Code>
</Box>
<VStack>
<Input
mt={2}
placeholder="Data Suffix (4 bytes)"
onChange={handleSetDataSuffix}
/>
</VStack>
</Flex>
)}
</CardBody>
<Button size="lg" colorScheme="telegram" onClick={startOnboarding}>
Start Onboarding
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet-sdk/src/CoinbaseWalletSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage';
import { getFavicon } from ':core/type/util';
import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy';
import { getCoinbaseInjectedProvider } from ':util/provider';
import { validatePreferences } from ':util/validatePreferences';

// for backwards compatibility
type CoinbaseWalletSDKOptions = Partial<AppMetadata>;
Expand All @@ -32,6 +33,7 @@ export class CoinbaseWalletSDK {
}

public makeWeb3Provider(preference: Preference = { options: 'all' }): ProviderInterface {
validatePreferences(preference);
const params = { metadata: this.metadata, preference };
return getCoinbaseInjectedProvider(params) ?? new CoinbaseWalletProvider(params);
}
Expand Down
52 changes: 15 additions & 37 deletions packages/wallet-sdk/src/core/provider/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ export interface AppMetadata {
appChainIds: number[];
}

type PostOnboardingAction = 'none' | 'onramp' | 'magicspend';

type OnrampPrefillOptions = {
contractAddress?: string;
amount: string;
chainId: number;
};
export type Attribution =
| {
auto: boolean;
dataSuffix?: never;
}
| {
auto?: never;
dataSuffix: `0x${string}`;
};

export type Preference = {
/**
Expand All @@ -60,38 +62,14 @@ export type Preference = {
*/
options: 'all' | 'smartWalletOnly' | 'eoaOnly';
/**
* @param postOnboardingAction
* @type {PostOnboardingAction}
* @description This option only applies to Coinbase Smart Wallet. Displays CTAs to the user based on the preference of the app.
* These CTAs are part of prebuilt UI components that are available to the Coinbase
* Smart Wallet.
*
* Possible values:
* - `none`: No action is recommended post-onboarding. (Default experience)
* - `onramp`: Recommends initiating the onramp flow, allowing users to prefill their account with an optional asset.
* - `magicspend`: Suggests linking the users retail Coinbase account for seamless transactions.
*/
postOnboardingAction?: PostOnboardingAction;
/**
* @param onrampPrefillOptions
* @type {OnrampPrefillOptions}
* @description This option only applies to Coinbase Smart Wallet. Requires `postOnboardingAction` to be set to `onramp`. When not configured,
* The onramp screen defaults to an asset selector with 0 as the initial amount.
*
* - Prefills the onramp flow with the specified asset, chain, and suggested amount, allowing users to prefill their account.
* - Ensure the asset and chain are supported by the onramp provider (e.g., Coinbase Pay - CBPay).
*
* See https://docs.cdp.coinbase.com/onramp/docs/layer2#available-assets for a list of supported assets and networks.
*/
onrampPrefillOptions?: OnrampPrefillOptions;
/**
* @param attributionDataSuffix
* @type {Hex}
* @param attribution
* @type {Attribution}
* @note Smart Wallet only
* @description This option only applies to Coinbase Smart Wallet. Data suffix to be appended to the initCode or executeBatch calldata
* Coinbase Smart Wallet expects a 4 byte hex string. If the suffix is not a 4 byte hex string, the Smart Wallet will not apply the data suffix.
* @description This option only applies to Coinbase Smart Wallet. When a valid data suffix is supplied, it is appended to the initCode and executeBatch calldata.
* Coinbase Smart Wallet expects a 16 byte hex string. If the data suffix is not a 16 byte hex string, the Smart Wallet will ignore the property. If auto is true,
* the Smart Wallet will generate a 16 byte hex string from the apps origin.
*/
attributionDataSuffix?: string;
attribution?: Attribution;
} & Record<string, unknown>;

export interface ConstructorOptions {
Expand Down
12 changes: 12 additions & 0 deletions packages/wallet-sdk/src/createCoinbaseWalletSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from ':core/provider/interface';
import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage';
import { checkCrossOriginOpenerPolicy } from ':util/crossOriginOpenerPolicy';
import { validatePreferences } from ':util/validatePreferences';

export type CreateCoinbaseWalletSDKOptions = Partial<AppMetadata> & {
preference?: Preference;
Expand All @@ -17,6 +18,11 @@ const DEFAULT_PREFERENCE: Preference = {
options: 'all',
};

/**
* Create a Coinbase Wallet SDK instance.
* @param params - Options to create a Coinbase Wallet SDK instance.
* @returns A Coinbase Wallet SDK object.
*/
export function createCoinbaseWalletSDK(params: CreateCoinbaseWalletSDKOptions) {
const versionStorage = new ScopedLocalStorage('CBWSDK');
versionStorage.setItem('VERSION', LIB_VERSION);
Expand All @@ -31,6 +37,12 @@ export function createCoinbaseWalletSDK(params: CreateCoinbaseWalletSDKOptions)
},
preference: Object.assign(DEFAULT_PREFERENCE, params.preference ?? {}),
};

/**
* Validate user supplied preferences. Throws if key/values are not valid.
*/
validatePreferences(options.preference);

let provider: ProviderInterface | null = null;

return {
Expand Down
Loading

0 comments on commit 3da9ef6

Please sign in to comment.