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

Update the attribution property #1420

Merged
merged 3 commits into from
Oct 7, 2024
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
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
Loading