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

v1.1: Building out crypto #269

Merged
merged 5 commits into from
Sep 14, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"eslint-plugin-prettier": "5.0.0",
"gh-pages": "6.0.0",
"jest": "29.6.4",
"jest-environment-jsdom": "^29.6.4",
"jest-environment-jsdom": "29.6.4",
"lerna": "4.0.0",
"prettier": "3.0.3",
"shx": "0.3.4",
Expand Down
1 change: 1 addition & 0 deletions packages/core/base/src/consts/embed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const embeddedCheckoutIFrameId = "crossmint-embedded-checkout.iframe";
1 change: 1 addition & 0 deletions packages/core/base/src/consts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./embed";
107 changes: 64 additions & 43 deletions packages/core/base/src/services/embed/crossmintIFrameService.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,93 @@
import { embeddedCheckoutIFrameId } from "@/consts";
import {
CrossmintEmbeddedCheckoutProps,
CrossmintEvent,
CrossmintEvents,
CrossmintInternalEvent,
CrossmintInternalEvents,
IncomingInternalEvent,
IncomingInternalEvents,
OutgoingInternalEvent,
} from "@/types";

import { getEnvironmentBaseUrl, isFiatEmbeddedCheckoutProps } from "../../utils";

// TODO: Emit updatable parameters
export function crossmintIFrameService(props: CrossmintEmbeddedCheckoutProps) {
return {
getUrl,
listenToEvents,
listenToInternalEvents,
};
}
const targetOrigin = getEnvironmentBaseUrl(props.environment);

function getUrl(props: CrossmintEmbeddedCheckoutProps) {
const baseUrl = getEnvironmentBaseUrl(props.environment);
const path = isFiatEmbeddedCheckoutProps(props) ? "/sdk/paymentElement" : "/sdk/2023-06-09/embeddedCheckout"; // TODO: v2.0 - remove '/sdk/paymentElement'
function getUrl(props: CrossmintEmbeddedCheckoutProps) {
const path = isFiatEmbeddedCheckoutProps(props) ? "/sdk/paymentElement" : "/sdk/2023-06-09/embeddedCheckout"; // TODO: v2.0 - remove '/sdk/paymentElement'

const queryParams = new URLSearchParams();
const queryParams = new URLSearchParams();

let key: keyof CrossmintEmbeddedCheckoutProps;
for (key in props) {
const value = props[key] as unknown;
const paramsToExclude = ["environment"];

if (!value || typeof value === "function") {
continue;
}
if (typeof value === "object") {
queryParams.append(key, JSON.stringify(value));
} else if (typeof value === "string") {
if (value === "undefined") {
let key: keyof CrossmintEmbeddedCheckoutProps;
for (key in props) {
const value = props[key] as unknown;

if (!value || typeof value === "function" || paramsToExclude.includes(key)) {
continue;
}
queryParams.append(key, value);
} else if (["boolean", "number"].includes(typeof value)) {
queryParams.append(key, value.toString());
if (typeof value === "object") {
queryParams.append(key, JSON.stringify(value));
} else if (typeof value === "string") {
if (value === "undefined") {
continue;
}
queryParams.append(key, value);
} else if (["boolean", "number"].includes(typeof value)) {
queryParams.append(key, value.toString());
}
}

return `${targetOrigin}${path}?${queryParams.toString()}`;
}

return `${baseUrl}${path}?${queryParams.toString()}`;
}
function _listenToEvents<EP = CrossmintEvent | CrossmintInternalEvent>(
callback: (event: MessageEvent<EP>) => void,
validEventTypes: {
[key: string]: CrossmintEvents | CrossmintInternalEvents;
}
) {
function _onEvent(event: MessageEvent) {
if (event.origin !== targetOrigin) {
console.log("[Crossmint] Received event from invalid origin", event.origin, targetOrigin);
return;
}
if (Object.values(validEventTypes).includes(event.data.type)) {
callback(event);
}
}

function _listenToEvents<EP = CrossmintEvent | CrossmintInternalEvent>(
callback: (event: MessageEvent<EP>) => void,
validEventTypes: {
[key: string]: CrossmintEvents | CrossmintInternalEvents;
window.addEventListener("message", _onEvent);
return () => {
window.removeEventListener("message", _onEvent);
};
}
) {
function _onEvent(event: MessageEvent) {
if (event.origin !== window.origin) {

const listenToEvents = (callback: (event: MessageEvent<CrossmintEvent>) => void) =>
_listenToEvents(callback, CrossmintEvents);
const listenToInternalEvents = (callback: (event: MessageEvent<IncomingInternalEvent>) => void) =>
_listenToEvents(callback, IncomingInternalEvents);

function emitInternalEvent(event: OutgoingInternalEvent) {
const iframe = document.getElementById(embeddedCheckoutIFrameId) as HTMLIFrameElement | null;
if (iframe == null) {
console.error("[Crossmint] Failed to find crossmint-embedded-checkout.iframe");
return;
}
if (Object.values(validEventTypes).includes(event.data.type)) {
callback(event);
try {
iframe.contentWindow?.postMessage(event, targetOrigin);
} catch (e) {
console.error("[Crossmint] Failed to emit internal event", event, e);
}
}

window.addEventListener("message", _onEvent);
return () => {
window.removeEventListener("message", _onEvent);
return {
getUrl,
listenToEvents,
listenToInternalEvents,
emitInternalEvent,
};
}

const listenToEvents = (callback: (event: MessageEvent<CrossmintEvent>) => void) =>
_listenToEvents(callback, CrossmintEvents);
const listenToInternalEvents = (callback: (event: MessageEvent<CrossmintInternalEvent>) => void) =>
_listenToEvents(callback, CrossmintInternalEvents);
6 changes: 6 additions & 0 deletions packages/core/base/src/types/embed/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export type CryptoEmbeddedCheckoutProps<
signer?: CryptoPaymentMethodSignerMap[PM];
};

export type CryptoEmbeddedCheckoutPropsWithSigner<
PM extends keyof CryptoPaymentMethodSignerMap = keyof CryptoPaymentMethodSignerMap,
> = CommonEmbeddedCheckoutProps<PM> & {
signer: CryptoPaymentMethodSignerMap[PM];
};

type CryptoPaymentMethodSignerMap = {
[CryptoPaymentMethod.ETH]: ETHEmbeddedCheckoutSigner;
[CryptoPaymentMethod.SOL]: SOLEmbeddedCheckoutSigner;
Expand Down
2 changes: 0 additions & 2 deletions packages/core/base/src/types/embed/fiat.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { CommonEmbeddedCheckoutProps } from ".";
import { Recipient } from "..";

export type FiatEmbeddedCheckoutProps = CommonEmbeddedCheckoutProps<"fiat"> & {
// TODO: Audit old params
cardWalletPaymentMethods?: CardWalletPaymentMethod | CardWalletPaymentMethod[] | "none";
emailInputOptions?: EmailInputOptions;
experimental?: EmbeddedCheckoutExperimentalOptions;
recipient?: Recipient;
};

export type CardWalletPaymentMethod = "apple-pay" | "google-pay";
Expand Down
2 changes: 2 additions & 0 deletions packages/core/base/src/types/embed/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Locale,
MintConfigs,
PaymentMethod,
Recipient,
UIConfig,
} from "..";
import { CryptoEmbeddedCheckoutProps } from "./crypto";
Expand All @@ -23,6 +24,7 @@ export type CommonEmbeddedCheckoutProps<PM extends PaymentMethod = PaymentMethod
uiConfig?: UIConfig;
whPassThroughArgs?: any;
projectId?: string;
recipient?: Recipient;
onEvent?(event: CrossmintEvent): any;
} & CollectionOrClientId;

Expand Down
18 changes: 14 additions & 4 deletions packages/core/base/src/types/events/internal/events.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// All internal events
export const CrossmintInternalEvents = {
PARAMS_UPDATE: "params-update",
export const IncomingInternalEvents = {
UI_HEIGHT_CHANGED: "ui:height.changed",
CRYPTO_PAYMENT_INCOMING_TRANSACTION: "crypto-payment:incoming-transaction",
} as const;
export type IncomingInternalEvents = (typeof IncomingInternalEvents)[keyof typeof IncomingInternalEvents];

export const OutgoingInternalEvents = {
PARAMS_UPDATE: "params-update",
CRYPTO_PAYMENT_USER_ACCEPTED: "crypto-payment:user-accepted",
CRYPTO_PAYMENT_USER_REJECTED: "crypto-payment:user-rejected",
CRYPTO_PAYMENT_INCOMING_TRANSACTION: "crypto-payment:incoming-transaction",
} as const;
export type OutgoingInternalEvents = (typeof OutgoingInternalEvents)[keyof typeof OutgoingInternalEvents];

// All internal events
export const CrossmintInternalEvents = {
...IncomingInternalEvents,
...OutgoingInternalEvents,
} as const;
export type CrossmintInternalEvents = (typeof CrossmintInternalEvents)[keyof typeof CrossmintInternalEvents];
17 changes: 13 additions & 4 deletions packages/core/base/src/types/events/internal/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { CrossmintInternalEvents } from "./events";
import { IncomingInternalEvents, OutgoingInternalEvents } from "./events";
import { CrossmintInternalEventMap } from "./payloads";

export * from "./events";
export * from "./payloads";

export type CrossmintInternalEvent = {
[K in CrossmintInternalEvents]: {
export type IncomingInternalEvent = {
[K in IncomingInternalEvents]: {
type: K;
payload: CrossmintInternalEventMap[K];
};
}[CrossmintInternalEvents];
}[IncomingInternalEvents];

export type OutgoingInternalEvent = {
[K in OutgoingInternalEvents]: {
type: K;
payload: CrossmintInternalEventMap[K];
};
}[OutgoingInternalEvents];

export type CrossmintInternalEvent = IncomingInternalEvent | OutgoingInternalEvent;
11 changes: 8 additions & 3 deletions packages/core/base/src/types/events/internal/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { EmptyObject } from "@/types/system";

import { CrossmintInternalEvents } from "./events";

export interface CrossmintInternalEventMap {
[CrossmintInternalEvents.PARAMS_UPDATE]: ParamsUpdatePayload;
interface IncomingInternalEventMap {
[CrossmintInternalEvents.UI_HEIGHT_CHANGED]: { height: number };
[CrossmintInternalEvents.CRYPTO_PAYMENT_INCOMING_TRANSACTION]: { serializedTransaction: string };
}

interface OutgoingInternalEventMap {
[CrossmintInternalEvents.PARAMS_UPDATE]: ParamsUpdatePayload;
[CrossmintInternalEvents.CRYPTO_PAYMENT_USER_ACCEPTED]: { txId: string };
[CrossmintInternalEvents.CRYPTO_PAYMENT_USER_REJECTED]: EmptyObject;
[CrossmintInternalEvents.CRYPTO_PAYMENT_INCOMING_TRANSACTION]: { serializedTransaction: string };
}

export type CrossmintInternalEventMap = IncomingInternalEventMap & OutgoingInternalEventMap;

// Params update
export type ParamsUpdatePayload = Partial<
Record<keyof Omit<FiatEmbeddedCheckoutProps, "onEvent" | "environment">, any>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/base/src/types/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Union<L extends unknown | undefined, R extends unknown | undefined> = L ext
export type NestedPaths<
T extends GenericObject,
Prev extends Primitive | undefined = undefined,
Path extends Primitive | undefined = undefined
Path extends Primitive | undefined = undefined,
> = {
[K in keyof T]: T[K] extends GenericObject
? NestedPaths<T[K], Union<Prev, Path>, Join<Path, K>>
Expand All @@ -44,7 +44,7 @@ export type NestedPaths<
*/
export type TypeFromPath<
T extends GenericObject,
Path extends string // Or, if you prefer, NestedPaths<T>
Path extends string, // Or, if you prefer, NestedPaths<T>
> = {
[K in Path]: K extends keyof T
? T[K]
Expand Down
7 changes: 7 additions & 0 deletions packages/core/base/src/utils/embed/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
CrossmintEmbeddedCheckoutProps,
CryptoEmbeddedCheckoutProps,
CryptoEmbeddedCheckoutPropsWithSigner,
CryptoPaymentMethod,
FiatEmbeddedCheckoutProps,
} from "../../types";
Expand All @@ -14,3 +15,9 @@ export function isCryptoEmbeddedCheckoutProps(
): props is CryptoEmbeddedCheckoutProps {
return (Object.values(CryptoPaymentMethod) as string[]).includes(props.paymentMethod ?? "");
}

export function isCryptoEmbeddedCheckoutPropsWithSigner(
props: CrossmintEmbeddedCheckoutProps
): props is CryptoEmbeddedCheckoutPropsWithSigner {
return isCryptoEmbeddedCheckoutProps(props) && props.signer != null;
}
39 changes: 32 additions & 7 deletions packages/starter/nextjs-starter/pages/payment-element.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import { useState } from "react";

import { CrossmintPaymentElement } from "@crossmint/client-sdk-react-ui";

export default function PaymentElementPage() {
const [count, setCount] = useState(1);

return (
<CrossmintPaymentElement
environment="http://localhost:3000"
collectionId="<COLLECTION_ID>"
projectId="<PROJECT_ID>"
mintConfig={{ totalPrice: "0.001", quantity: "1" }}
recipient={{ email: "[email protected]" }}
/>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: "20px",
}}
>
<button onClick={() => setCount(count + 1)}>Increment count: {count}</button>
<CrossmintPaymentElement
environment="https://crossmint-main-git-checkout-embedded-new-element-p3-crossmint.vercel.app"
clientId="db218e78-d042-4761-83af-3c4e5e6659dd"
recipient={{ wallet: "maxfQWBno84Zfu4sXgmjYvsvLn4LzGFSgSkFMFuzved" }}
mintConfig={{
testCount: count,
}}
paymentMethod="ETH"
signer={{
address: "0xdC9bb9929b79b62d630A7C3568c979a2843eFd8b",
signAndSendTransaction: async (tx) => {
return "0x1234";
},
}}
onEvent={(event) => {
console.log(event);
}}
/>
</div>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useEffect, useState } from "react";

import { crossmintIFrameService } from "@crossmint/client-sdk-base";
import { CrossmintInternalEvent, IncomingInternalEvent, crossmintIFrameService } from "@crossmint/client-sdk-base";
import { CrossmintEmbeddedCheckoutProps } from "@crossmint/client-sdk-base";

export default function CrossmintEmbeddedCheckoutIFrame(props: CrossmintEmbeddedCheckoutProps) {
const { getUrl, listenToEvents, listenToInternalEvents } = crossmintIFrameService(props);
type CrossmintEmbeddedCheckoutIFrameProps = CrossmintEmbeddedCheckoutProps & {
onInternalEvent?: (event: IncomingInternalEvent) => void;
};

export default function CrossmintEmbeddedCheckoutIFrame({
onInternalEvent,
...props
}: CrossmintEmbeddedCheckoutIFrameProps) {
const { getUrl, listenToEvents, listenToInternalEvents, emitInternalEvent } = crossmintIFrameService(props);

const [height, setHeight] = useState(0);
const [url] = useState(getUrl(props));
Expand All @@ -25,13 +32,11 @@ export default function CrossmintEmbeddedCheckoutIFrame(props: CrossmintEmbedded
const clearListener = listenToInternalEvents((event) => {
const { type, payload } = event.data;

switch (type) {
case "ui:height.changed":
setHeight(payload.height);
break;
default:
return;
if (type === "ui:height.changed") {
setHeight(payload.height);
}

onInternalEvent?.(event.data);
});

return () => {
Expand Down
Loading