Skip to content

Commit

Permalink
v1.1: Building out crypto (#269)
Browse files Browse the repository at this point in the history
* save

* recip

* more build out

* testing page

* ?
  • Loading branch information
mPaella authored Sep 14, 2023
1 parent 7f6af83 commit a20e808
Show file tree
Hide file tree
Showing 22 changed files with 249 additions and 107 deletions.
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

0 comments on commit a20e808

Please sign in to comment.