From 89baf8bd57659184ee94afab37449cf3d7de53ec Mon Sep 17 00:00:00 2001 From: mPaella <93682696+mPaella@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:43:57 -0400 Subject: [PATCH] hosted v3: init (#831) * readme: remove beta (#826) * Headless Auth: POC demo (#813) * Headless Auth: POC demo * Added discord auth, added embedded form, added dialog styles * added merge changes * fixed styles and added stytch token call * remove console logs * removed unused dep * moved oauth url stuff to backend * removed embedded component factory and replace with hook component * replaced component from hook as exported component * finished farcaster login method * descoped any web3 changes to separate branch * removed any login methods to use default * added test for authformprovider * removed unused code * review feedback * prefetch oauth url on auth form mount * added more efficient query, removed discord, improved embedded component export * merge changes with main * made changes to correspond to recent backend changes * Auth: add Farcaster auth method to demo (#827) Auth: add farcaster auth method to demo * Rename showWalletModals to showPasskeyHelpers (#795) * Rename showWalletModals to showPasskeyHelpers * lint fix * added changeset * Changesets: Removal of auth iframe, updated common auth sdk (#829) added changesets * embed: dynamic.xyz stuff (#832) * embed: dynamicxyz env * save * changeset * Release packages (#830) Co-authored-by: github-actions[bot] * Also send api key in header in oauth fetch to support backend change (#834) * Release packages (#833) Co-authored-by: github-actions[bot] * embed: add receipt email props (#835) * embed: add receipt email props * changeset * Release packages (#836) Co-authored-by: github-actions[bot] * hosted v3 :init * straight svg * rem --------- Co-authored-by: Jonathan Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alberto Elias --- apps/payments/nextjs/.gitignore | 2 + .../HostedCheckoutV3ClientProviders.tsx | 13 ++++ .../nextjs/pages/hosted-checkout/v3.tsx | 49 ++++++++++++ .../v3/crossmintEmbeddedCheckoutV3Service.ts | 23 +----- .../client/base/src/services/hosted/index.ts | 2 + .../crossmintHostedCheckoutOverlayService.ts | 72 ++++++++++++++++++ .../v3/crossmintHostedCheckoutV3Service.ts | 74 +++++++++++++++++++ .../base/src/services/hosted/v3/index.ts | 1 + .../client/base/src/types/hosted/index.ts | 2 + .../v3/CrossmintHostedCheckoutV3Props.ts | 26 +++++++ .../client/base/src/types/hosted/v3/index.ts | 1 + .../src/utils/appendObjectToQueryParams.ts | 21 ++++++ .../embed/v3/EmbeddedCheckoutV3IFrame.tsx | 6 +- .../react-ui/src/components/hosted/index.ts | 2 + .../hosted/v3/CrossmintHostedCheckoutV3.tsx | 42 +++++++++++ .../src/components/hosted/v3/index.ts | 1 + packages/client/window/src/windows/Popup.ts | 19 ++++- 17 files changed, 330 insertions(+), 26 deletions(-) create mode 100644 apps/payments/nextjs/components/hosted-v3/HostedCheckoutV3ClientProviders.tsx create mode 100644 apps/payments/nextjs/pages/hosted-checkout/v3.tsx create mode 100644 packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutOverlayService.ts create mode 100644 packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutV3Service.ts create mode 100644 packages/client/base/src/services/hosted/v3/index.ts create mode 100644 packages/client/base/src/types/hosted/v3/CrossmintHostedCheckoutV3Props.ts create mode 100644 packages/client/base/src/types/hosted/v3/index.ts create mode 100644 packages/client/base/src/utils/appendObjectToQueryParams.ts create mode 100644 packages/client/ui/react-ui/src/components/hosted/v3/CrossmintHostedCheckoutV3.tsx create mode 100644 packages/client/ui/react-ui/src/components/hosted/v3/index.ts diff --git a/apps/payments/nextjs/.gitignore b/apps/payments/nextjs/.gitignore index 1437c53f7..fb982c946 100644 --- a/apps/payments/nextjs/.gitignore +++ b/apps/payments/nextjs/.gitignore @@ -32,3 +32,5 @@ yarn-error.log* # vercel .vercel + +certificates \ No newline at end of file diff --git a/apps/payments/nextjs/components/hosted-v3/HostedCheckoutV3ClientProviders.tsx b/apps/payments/nextjs/components/hosted-v3/HostedCheckoutV3ClientProviders.tsx new file mode 100644 index 000000000..88091e59a --- /dev/null +++ b/apps/payments/nextjs/components/hosted-v3/HostedCheckoutV3ClientProviders.tsx @@ -0,0 +1,13 @@ +import { CrossmintCheckoutProvider, CrossmintProvider } from "@crossmint/client-sdk-react-ui"; +import type { ReactNode } from "react"; + +export function HostedCheckoutV3ClientProviders({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/payments/nextjs/pages/hosted-checkout/v3.tsx b/apps/payments/nextjs/pages/hosted-checkout/v3.tsx new file mode 100644 index 000000000..cd9cbf878 --- /dev/null +++ b/apps/payments/nextjs/pages/hosted-checkout/v3.tsx @@ -0,0 +1,49 @@ +import { CrossmintHostedCheckout_Alpha } from "@crossmint/client-sdk-react-ui"; +import { HostedCheckoutV3ClientProviders } from "../../components/hosted-v3/HostedCheckoutV3ClientProviders"; + +export default function HostedCheckoutV3Page() { + return ( +
+
+ + + +
+
+ ); +} diff --git a/packages/client/base/src/services/embed/v3/crossmintEmbeddedCheckoutV3Service.ts b/packages/client/base/src/services/embed/v3/crossmintEmbeddedCheckoutV3Service.ts index 21648eea8..05296a1b4 100644 --- a/packages/client/base/src/services/embed/v3/crossmintEmbeddedCheckoutV3Service.ts +++ b/packages/client/base/src/services/embed/v3/crossmintEmbeddedCheckoutV3Service.ts @@ -1,5 +1,6 @@ import type { CrossmintEmbeddedCheckoutV3Props } from "@/types/embed/v3/CrossmintEmbeddedCheckoutV3Props"; import { embeddedCheckoutV3IncomingEvents, embeddedCheckoutV3OutgoingEvents } from "@/types/embed/v3/events"; +import { appendObjectToQueryParams } from "@/utils/appendObjectToQueryParams"; import { IFrameWindow } from "@crossmint/client-sdk-window"; import type { CrossmintApiClient } from "@crossmint/common-sdk-base"; @@ -14,27 +15,7 @@ export function crossmintEmbeddedCheckoutV3Service({ apiClient }: CrossmintEmbed const urlWithPath = apiClient.buildUrl("/sdk/2024-03-05/embedded-checkout"); const queryParams = new URLSearchParams(); - let key: keyof CrossmintEmbeddedCheckoutV3Props; - for (key in props) { - const value = props[key] as unknown; - - if (!value || typeof value === "function") { - continue; - } - if (typeof value === "object") { - queryParams.append( - key, - JSON.stringify(value, (key, val) => (typeof val === "function" ? "function" : val)) - ); - } 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()); - } - } + appendObjectToQueryParams(queryParams, props); queryParams.append("apiKey", apiClient.crossmint.apiKey); queryParams.append("sdkMetadata", JSON.stringify(sdkMetadata)); diff --git a/packages/client/base/src/services/hosted/index.ts b/packages/client/base/src/services/hosted/index.ts index b89b94fd1..57f73c0db 100644 --- a/packages/client/base/src/services/hosted/index.ts +++ b/packages/client/base/src/services/hosted/index.ts @@ -1,2 +1,4 @@ export * from "./crossmintModalService"; export * from "./crossmintPayButtonService"; + +export * from "./v3"; diff --git a/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutOverlayService.ts b/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutOverlayService.ts new file mode 100644 index 000000000..8d3a0a3a9 --- /dev/null +++ b/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutOverlayService.ts @@ -0,0 +1,72 @@ +import type { PopupWindow } from "@crossmint/client-sdk-window"; + +const OVERLAY_ID = "crossmint-hosted-checkout-v3-overlay"; + +export function crossmintHostedCheckoutOverlayService() { + function createOverlay(windowClient: ReturnType) { + const overlay = document.createElement("div"); + overlay.setAttribute("id", OVERLAY_ID); + Object.assign(overlay.style, { + width: "100vw", + height: "100vh", + "background-color": "rgba(0, 0, 0, 0.5)", + inset: 0, + position: "fixed", + "z-index": "99999999", + opacity: "0", + transition: "opacity 0.25s ease-in-out", + display: "flex", + "flex-direction": "column", + "justify-content": "center", + "align-items": "center", + padding: "20px", + }); + overlay.innerHTML = INNER_HTML; + document.body.appendChild(overlay); + + setTimeout(() => { + overlay.style.opacity = "1"; + }, 10); + + const interval = setInterval(() => { + if (windowClient.window.closed) { + clearInterval(interval); + removeOverlay(); + } + }, 250); + + overlay.addEventListener("click", () => { + clearInterval(interval); + removeOverlay(); + }); + } + + function removeOverlay() { + const overlay = document.getElementById(OVERLAY_ID); + if (overlay) { + overlay.style.opacity = "0"; + setTimeout(() => { + overlay.remove(); + }, 250); + } + } + + return { + create: createOverlay, + remove: removeOverlay, + }; +} + +const INNER_HTML = ` + + + + + + + + + + +

Continue your purchase in the secure Crossmint window

+ `; diff --git a/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutV3Service.ts b/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutV3Service.ts new file mode 100644 index 000000000..0defa539d --- /dev/null +++ b/packages/client/base/src/services/hosted/v3/crossmintHostedCheckoutV3Service.ts @@ -0,0 +1,74 @@ +import type { CrossmintHostedCheckoutV3Props } from "@/types/hosted/v3/CrossmintHostedCheckoutV3Props"; +import { appendObjectToQueryParams } from "@/utils/appendObjectToQueryParams"; +import { PopupWindow } from "@crossmint/client-sdk-window"; +import type { CrossmintApiClient } from "@crossmint/common-sdk-base"; +import { crossmintHostedCheckoutOverlayService } from "./crossmintHostedCheckoutOverlayService"; + +export type CrossmintHostedCheckoutV3ServiceProps = { + apiClient: CrossmintApiClient; + hostedCheckoutProps: CrossmintHostedCheckoutV3Props; +}; + +export function crossmintHostedCheckoutV3Service({ + apiClient, + hostedCheckoutProps, +}: CrossmintHostedCheckoutV3ServiceProps) { + const overlayService = crossmintHostedCheckoutOverlayService(); + + function getUrl(props: CrossmintHostedCheckoutV3Props) { + const urlWithPath = apiClient.buildUrl("/sdk/2024-03-05/hosted-checkout"); + const queryParams = new URLSearchParams(); + + appendObjectToQueryParams(queryParams, props); + + queryParams.append("apiKey", apiClient.crossmint.apiKey); + queryParams.append("sdkMetadata", JSON.stringify(apiClient["internalConfig"].sdkMetadata)); + + return `${urlWithPath}?${queryParams.toString()}`; + } + + function createPopupClient() { + const url = getUrl(hostedCheckoutProps); + return PopupWindow.initSync(url, { + width: 450, + height: 750, + crossOrigin: true, + }); + } + + // TODO: Implement new tab client + function createNewTabClient(): ReturnType { + throw new Error("Not implemented"); + } + + // TODO: Implement same tab client + function createSameTabClient(): ReturnType { + throw new Error("Not implemented"); + } + + function createWindow() { + const displayType = hostedCheckoutProps.appearance?.display || "popup"; + let windowClient: ReturnType; + switch (displayType) { + case "popup": + windowClient = createPopupClient(); + break; + case "same-tab": + windowClient = createSameTabClient(); + break; + case "new-tab": + windowClient = createNewTabClient(); + break; + default: + throw new Error(`Invalid display type: ${displayType}`); + } + + if (hostedCheckoutProps.appearance?.overlay?.enabled !== false && displayType !== "same-tab") { + overlayService.create(windowClient); + } + } + + return { + createWindow, + }; +} diff --git a/packages/client/base/src/services/hosted/v3/index.ts b/packages/client/base/src/services/hosted/v3/index.ts new file mode 100644 index 000000000..cd7526db7 --- /dev/null +++ b/packages/client/base/src/services/hosted/v3/index.ts @@ -0,0 +1 @@ +export * from "./crossmintHostedCheckoutV3Service"; diff --git a/packages/client/base/src/types/hosted/index.ts b/packages/client/base/src/types/hosted/index.ts index 1a2142282..973ccd198 100644 --- a/packages/client/base/src/types/hosted/index.ts +++ b/packages/client/base/src/types/hosted/index.ts @@ -1,3 +1,5 @@ +export * from "./v3"; + import type { Currency, Locale, PaymentMethod } from ".."; import type { CaseInsensitive } from "../system"; diff --git a/packages/client/base/src/types/hosted/v3/CrossmintHostedCheckoutV3Props.ts b/packages/client/base/src/types/hosted/v3/CrossmintHostedCheckoutV3Props.ts new file mode 100644 index 000000000..68964d419 --- /dev/null +++ b/packages/client/base/src/types/hosted/v3/CrossmintHostedCheckoutV3Props.ts @@ -0,0 +1,26 @@ +import type { Locale } from "@/types"; +import type { EmbeddedCheckoutV3LineItem, EmbeddedCheckoutV3Payment, EmbeddedCheckoutV3Recipient } from "@/types/embed"; + +export interface CrossmintHostedCheckoutV3Props { + receipient?: EmbeddedCheckoutV3Recipient; + locale?: Locale; + webhookPassthroughData?: any; + lineItems: EmbeddedCheckoutV3LineItem | EmbeddedCheckoutV3LineItem[]; + payment: EmbeddedCheckoutV3Payment; + appearance?: CrossmintHostedCheckoutV3Appearance; +} + +export interface CrossmintHostedCheckoutV3Appearance { + theme?: "light" | "dark"; + variables?: CrossmintHostedCheckoutV3AppearanceVariables; + overlay?: CrossmintHostedCheckoutV3OverlayOptions; + display?: "popup" | "same-tab" | "new-tab"; +} + +export interface CrossmintHostedCheckoutV3AppearanceVariables { + colors?: { + accent?: string; + }; +} + +export type CrossmintHostedCheckoutV3OverlayOptions = { enabled: boolean }; diff --git a/packages/client/base/src/types/hosted/v3/index.ts b/packages/client/base/src/types/hosted/v3/index.ts new file mode 100644 index 000000000..73f8448b4 --- /dev/null +++ b/packages/client/base/src/types/hosted/v3/index.ts @@ -0,0 +1 @@ +export * from "./CrossmintHostedCheckoutV3Props"; diff --git a/packages/client/base/src/utils/appendObjectToQueryParams.ts b/packages/client/base/src/utils/appendObjectToQueryParams.ts new file mode 100644 index 000000000..26250b61c --- /dev/null +++ b/packages/client/base/src/utils/appendObjectToQueryParams.ts @@ -0,0 +1,21 @@ +export function appendObjectToQueryParams>(queryParams: URLSearchParams, props: T): void { + for (const [key, value] of Object.entries(props)) { + if (!value || typeof value === "function") { + continue; + } + + if (typeof value === "object") { + queryParams.append( + key, + JSON.stringify(value, (_, val) => (typeof val === "function" ? "function" : val)) + ); + } 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()); + } + } +} diff --git a/packages/client/ui/react-ui/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx b/packages/client/ui/react-ui/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx index afd69fbc7..eda07906c 100644 --- a/packages/client/ui/react-ui/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx +++ b/packages/client/ui/react-ui/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx @@ -16,7 +16,7 @@ export function EmbeddedCheckoutV3IFrame(props: CrossmintEmbeddedCheckoutV3Props const { crossmint } = useCrossmint(); const apiClient = createCrossmintApiClient(crossmint); - const embedV3Service = crossmintEmbeddedCheckoutV3Service({ apiClient }); + const embeddedCheckoutService = crossmintEmbeddedCheckoutV3Service({ apiClient }); const ref = useRef(null); @@ -25,7 +25,7 @@ export function EmbeddedCheckoutV3IFrame(props: CrossmintEmbeddedCheckoutV3Props if (!iframe || iframeClient) { return; } - setIframeClient(embedV3Service.iframe.createClient(iframe)); + setIframeClient(embeddedCheckoutService.iframe.createClient(iframe)); }, [ref.current, iframeClient]); useEffect(() => { @@ -43,7 +43,7 @@ export function EmbeddedCheckoutV3IFrame(props: CrossmintEmbeddedCheckoutV3Props <>