Skip to content

Commit

Permalink
hosted v3: init (#831)
Browse files Browse the repository at this point in the history
* 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] <github-actions[bot]@users.noreply.github.com>

* Also send api key in header in oauth fetch to support backend change (#834)

* Release packages (#833)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* embed: add receipt email props (#835)

* embed: add receipt email props

* changeset

* Release packages (#836)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* hosted v3 :init

* straight svg

* rem

---------

Co-authored-by: Jonathan <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Alberto Elias <[email protected]>
  • Loading branch information
5 people authored Oct 26, 2024
1 parent 7d95b83 commit 89baf8b
Show file tree
Hide file tree
Showing 17 changed files with 330 additions and 26 deletions.
2 changes: 2 additions & 0 deletions apps/payments/nextjs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ yarn-error.log*

# vercel
.vercel

certificates
Original file line number Diff line number Diff line change
@@ -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 (
<CrossmintProvider
overrideBaseUrl="https://dserver.maxf.io"
apiKey="ck_development_5Nx4yZXetY5QVxJUp8kg1vaUoRvPtWUiFKQPiMqDmquohnkTJyXc1ynAw6XA6NiiGekhwtTMebDc9wbcFVS5ePxDqLFa9qkiXHuRH8n2igPAUQ9xXsyQBizWvRVdTx2Koy9s4qm8kaAiSQj4CtwiyY8EEUeAqWcsVVLAhjYCSDpViCMfqRjfM4FcnqgSieoRqaE7A4sLJVQWybnYbWJA8cXE"
>
<CrossmintCheckoutProvider>{children}</CrossmintCheckoutProvider>
</CrossmintProvider>
);
}
49 changes: 49 additions & 0 deletions apps/payments/nextjs/pages/hosted-checkout/v3.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
padding: "20px",
// backgroundColor: "black",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
width: "100%",
maxWidth: "450px",
}}
>
<HostedCheckoutV3ClientProviders>
<CrossmintHostedCheckout_Alpha
lineItems={{
collectionLocator: "crossmint:91e3ae09-2d59-4d21-a811-058732351847",
// callData: {
// totalPrice: "0.001",
// quantity: 1,
// },
}}
payment={{
crypto: {
enabled: false,
},
fiat: {
enabled: true,
},
defaultMethod: "fiat",
}}
/>
</HostedCheckoutV3ClientProviders>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions packages/client/base/src/services/hosted/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from "./crossmintModalService";
export * from "./crossmintPayButtonService";

export * from "./v3";
Original file line number Diff line number Diff line change
@@ -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<typeof PopupWindow.initSync>) {
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 = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 459.2 86" style="width: 100%; max-width: 200px;">
<g>
<g>
<path fill="white" d="M372.7 9.5c0-3.4 2.7-6.1 6-6.1S385 6 385 9.5s-2.8 6-6.2 6-6-2.6-6-6m77.7 1.9V24h8.6v8.8h-8.6V52q-.1 5.4 5.3 5.2c1.3 0 3-.2 3.4-.3v8.2c-.6.2-2.5 1-6 1-7.7 0-12.5-4.7-12.5-12.5V33H433v-9h8.5V11.4zm-338 44.4q3 5 7.8 7.9 5 2.8 11.3 2.8 4.5 0 8.4-1.5 3.7-1.7 6.6-4.4 2.8-2.9 4-6.4l-8.9-4a10.5 10.5 0 0 1-10.1 7 11 11 0 0 1-6-1.5q-2.4-1.7-4-4.5-1.5-2.9-1.4-6.6a12 12 0 0 1 1.4-6.5q1.5-3 4-4.5a11 11 0 0 1 6-1.6q3.5 0 6.2 2 2.8 1.9 4 5l8.8-3.8q-1.2-3.9-4-6.5-2.9-2.8-6.7-4.3-3.9-1.6-8.3-1.6-6.3 0-11.3 2.8a21 21 0 0 0-7.8 7.8q-2.8 5-2.8 11.2c0 6.2 1 7.9 2.8 11.2m53-32.1h-9.4v41.9h10V42.3q0-4.8 2.7-7.4 2.5-2.7 6.9-2.7h3.6v-9H177q-4.5 0-7.6 1.9a10 10 0 0 0-3.8 4.5z"/>
<path d="M202.9 22.8c12.4 0 21.6 9.3 21.6 22s-9.1 22-21.6 22-21.5-9.2-21.5-22 9.1-22 21.5-22m0 35.2c6.1 0 11.6-4.5 11.6-13.2s-5.5-13-11.6-13-11.6 4.4-11.6 13S196.8 58 203 58" fill-rule="evenodd" fill="white"/>
<path fill="white" d="m236.4 52-8.6 2.4c.5 4.6 5 12.5 17.1 12.5 10.6 0 15.7-7 15.7-13.3s-4.1-11-12-12.6l-6.3-1.3q-4-1-4.1-4.4c0-2.5 2.4-4.7 6-4.7 5.5 0 7.3 3.8 7.6 6.2l8.4-2.4c-.7-4.1-4.5-11.6-16-11.6-8.7 0-15.3 6.1-15.3 13.4 0 5.7 3.8 10.5 11.1 12l6.2 1.4q4.9 1.2 4.8 4.6c0 2.6-2 4.8-6.2 4.8-5.3 0-8-3.3-8.4-7m27.6 2.4 8.5-2.4c.4 3.7 3.2 7 8.5 7 4 0 6.2-2.2 6.2-4.7q.1-3.6-4.9-4.6l-6.1-1.4c-7.3-1.6-11.2-6.4-11.2-12.1 0-7.2 6.7-13.4 15.3-13.4 11.6 0 15.3 7.5 16 11.6l-8.4 2.4c-.3-2.4-2-6.2-7.6-6.2-3.5 0-6 2.2-6 4.7q.2 3.5 4.2 4.4l6.3 1.3c7.8 1.7 12 6.5 12 12.6s-5.2 13.3-15.8 13.3c-12 0-16.6-7.8-17-12.5m48.6-30.7h-9.5v41.9h10.1V41q0-2.7 1-4.8a8 8 0 0 1 7-4.2 8 8 0 0 1 7.2 4.2q1 2.1 1 4.8v24.6h10.1V41q0-2.7 1-4.8a8 8 0 0 1 7-4.2q2.6 0 4.4 1.2 1.8 1 2.8 3 1 2.1 1 4.8v24.6h10v-27q0-4.5-2-8.2a14 14 0 0 0-5.5-5.6q-3.5-2-8-2-5 0-8.8 2.5-2.4 1.6-4.1 4.2-1.4-2.3-3.9-4a16 16 0 0 0-9-2.7q-4.8 0-8.3 2.2-2.2 1.5-3.5 4zm71.1 41.9h-9.8V24.1h9.8zm8.8-41.6v41.6h10V41.3q0-2.8 1.1-4.8a8 8 0 0 1 7.3-4.2 8 8 0 0 1 7.4 4.2q1 2 1 4.7v24.4h10V38.9q0-4.6-2-8.2a14 14 0 0 0-5.5-5.6q-3.4-2-8.1-2c-4.7 0-5.5.7-7.8 2q-2.5 1.6-4 4v-5z"/>
<path d="M70 48.3A62 62 0 0 0 47.5 43c7.7-.6 22.8-2.7 29-10.4C86.6 25.2 86 .3 86 .3S62.3-2.4 52 9.4c-6.5 6.4-8.4 17.5-9 25.2-.6-7.7-2.6-18.8-9-25.2C23.7-2.4 0 .3 0 .3S-.3 15.8 4.4 26C6.8 31 11 35.1 16 37.6c7.2 3.7 16.8 5 22.4 5.4-5.6.4-15.2 1.7-22.4 5.3C11 51 6.8 55 4.4 60-.3 70.2 0 85.7 0 85.7s23.7 2.6 34-9.2c6.4-6.3 8.4-17.5 9-25.2.6 7.8 2.6 18.9 9 25.3 10.3 11.7 34 9.1 34 9.1s.3-15.6-4.5-25.7A25 25 0 0 0 70 48.3m.8 21.9c-.1 0-12.5-3.6-28.3-24.7A195 195 0 0 0 15 71.8l-.5.6.2-.8c0-.1 3.7-12.8 25.7-29C38.3 39.4 33.3 32.2 14 15l-.4-.4.6.1c.4 0 10.3 1.6 28.5 25l.6 1c2.6-1.8 10-7.1 26.7-26l.4-.4-.1.6c0 .4-1.6 10.3-24.9 28.4A193 193 0 0 0 71 69.9l.6.5z" fill="white" fill-rule="evenodd"/>
</g>
</g>
</svg>
<p style="color: white; font-size: 16px; text-align: center; max-width: 400px;">Continue your purchase in the secure Crossmint window</p>
`;
Original file line number Diff line number Diff line change
@@ -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<typeof PopupWindow.initSync> {
throw new Error("Not implemented");
}

// TODO: Implement same tab client
function createSameTabClient(): ReturnType<typeof PopupWindow.initSync> {
throw new Error("Not implemented");
}

function createWindow() {
const displayType = hostedCheckoutProps.appearance?.display || "popup";
let windowClient: ReturnType<typeof PopupWindow.initSync>;
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,
};
}
1 change: 1 addition & 0 deletions packages/client/base/src/services/hosted/v3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./crossmintHostedCheckoutV3Service";
2 changes: 2 additions & 0 deletions packages/client/base/src/types/hosted/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./v3";

import type { Currency, Locale, PaymentMethod } from "..";
import type { CaseInsensitive } from "../system";

Expand Down
Original file line number Diff line number Diff line change
@@ -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 };
1 change: 1 addition & 0 deletions packages/client/base/src/types/hosted/v3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CrossmintHostedCheckoutV3Props";
21 changes: 21 additions & 0 deletions packages/client/base/src/utils/appendObjectToQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function appendObjectToQueryParams<T extends Record<string, any>>(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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLIFrameElement>(null);

Expand All @@ -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(() => {
Expand All @@ -43,7 +43,7 @@ export function EmbeddedCheckoutV3IFrame(props: CrossmintEmbeddedCheckoutV3Props
<>
<iframe
ref={ref}
src={embedV3Service.iframe.getUrl(props)}
src={embeddedCheckoutService.iframe.getUrl(props)}
id="crossmint-embedded-checkout.iframe"
role="crossmint-embedded-checkout.iframe"
allow="payment *"
Expand Down
2 changes: 2 additions & 0 deletions packages/client/ui/react-ui/src/components/hosted/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./CrossmintPayButton";

export * from "./v3";
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useCrossmint } from "@/hooks";
import { createCrossmintApiClient } from "@/utils/createCrossmintApiClient";
import { crossmintHostedCheckoutV3Service, type CrossmintHostedCheckoutV3Props } from "@crossmint/client-sdk-base";
import type { MouseEvent, JSX } from "react";

export type CrossmintHostedCheckoutV3ReactProps = CrossmintHostedCheckoutV3Props & JSX.IntrinsicElements["button"];

export function CrossmintHostedCheckout_Alpha(props: CrossmintHostedCheckoutV3ReactProps) {
const { crossmint } = useCrossmint();
const apiClient = createCrossmintApiClient(crossmint);

// separate custom props from jsx button props
const { receipient, locale, webhookPassthroughData, lineItems, payment, appearance, ...buttonProps } = props;
const customProps: CrossmintHostedCheckoutV3Props = {
receipient,
locale,
webhookPassthroughData,
lineItems,
payment,
appearance,
};

const hostedCheckoutService = crossmintHostedCheckoutV3Service({ apiClient, hostedCheckoutProps: customProps });

const { onClick, ...restButtonProps } = buttonProps;

function _onClick(e: MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
hostedCheckoutService.createWindow();

if (onClick) {
onClick(e);
}
}

return (
<button onClick={_onClick} {...restButtonProps}>
Pay with Crossmint
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CrossmintHostedCheckoutV3";
Loading

0 comments on commit 89baf8b

Please sign in to comment.