Skip to content

feat: hosted v3 #839

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

Merged
merged 7 commits into from
Nov 21, 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
8 changes: 8 additions & 0 deletions .changeset/sharp-boats-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@crossmint/client-sdk-react-ui": minor
"@crossmint/client-sdk-window": minor
"@crossmint/client-sdk-nextjs-starter": minor
"@crossmint/client-sdk-base": minor
---

feat: hosted v3 alpha
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
Expand Up @@ -27,9 +27,10 @@ export function EmbeddedCheckoutV3Content() {
function CrossmintEmbeddedCheckoutWrapper() {
return (
<CrossmintEmbeddedCheckout_Alpha
// recipient={{
// email: "[email protected]",
// }}
recipient={{
// email: "[email protected]",
walletAddress: "0x8b821dd648599B0D093F55B5BaAA48c709ec455A",
}}
lineItems={{
collectionLocator: "crossmint:206b3146-f526-444e-bd9d-0607d581b0e9",
callData: {
Expand All @@ -38,6 +39,7 @@ function CrossmintEmbeddedCheckoutWrapper() {
},
}}
payment={{
receiptEmail: "[email protected]",
crypto: {
enabled: true,
},
Expand Down
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_5ZUNkuhjP8aYZEgUTDfWToqFpo5zakEqte1db4pHZgPAVKZ9JuTQKmeRbn1gv7zYCoZrRNYy4CnM7A3AMHQxFKA2BsSVeZbKEvXXY7126Th68mXhTg6oxHJpC2kuw9Q1HasVLX9LM67FoYSTRtTUUEzP93GUSEmeG5CZG7Lbop4oAQ7bmZUKTGmqN9L9wxP27CH13WaTBsrqxUJkojbKUXEd"
>
<CrossmintCheckoutProvider>{children}</CrossmintCheckoutProvider>
</CrossmintProvider>
);
}
109 changes: 109 additions & 0 deletions apps/payments/nextjs/pages/hosted-checkout/v3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { CrossmintHostedCheckout_Alpha } from "@crossmint/client-sdk-react-ui";
import { HostedCheckoutV3ClientProviders } from "../../components/hosted-v3/HostedCheckoutV3ClientProviders";
import type { CrossmintHostedCheckoutV3ButtonTheme, Locale } from "@crossmint/client-sdk-base";

const LOCALE: Locale = "en-US";

export default function HostedCheckoutV3Page() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
}}
>
<HostedCheckoutV3ClientProviders>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
width: "100%",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
}}
>
{createCrossmintButtonSection("light", LOCALE, true, true)}
{createCrossmintButtonSection("dark", LOCALE, true, true)}
{createCrossmintButtonSection("crossmint", LOCALE, true, true)}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
}}
>
{createCrossmintButtonSection("light", LOCALE, true, false)}
{createCrossmintButtonSection("dark", LOCALE, true, false)}
{createCrossmintButtonSection("crossmint", LOCALE, true, false)}
</div>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "start",
}}
>
{createCrossmintButtonSection("light", LOCALE, false, true)}
{createCrossmintButtonSection("dark", LOCALE, false, true)}
{createCrossmintButtonSection("crossmint", LOCALE, false, true)}
</div>
</div>
</HostedCheckoutV3ClientProviders>
</div>
);
}

function createCrossmintButtonSection(
buttonTheme: CrossmintHostedCheckoutV3ButtonTheme,
locale: Locale,
cryptoEnabled: boolean,
fiatEnabled: boolean
) {
return (
<div
style={{
backgroundColor: buttonTheme === "light" ? "black" : "white",
width: "100%",
display: "flex",
justifyContent: "center",
padding: "20px",
}}
>
<CrossmintHostedCheckout_Alpha
locale={locale}
lineItems={{
collectionLocator: "crossmint:206b3146-f526-444e-bd9d-0607d581b0e9",
callData: {
totalPrice: "0.001",
quantity: 1,
},
}}
payment={{
crypto: {
enabled: cryptoEnabled,
},
fiat: {
enabled: fiatEnabled,
},
defaultMethod: fiatEnabled ? "fiat" : "crypto",
}}
appearance={{
theme: {
button: buttonTheme,
},
}}
/>
</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,74 @@
import type { NewTabWindow, PopupWindow } from "@crossmint/client-sdk-window";

const OVERLAY_ID = "crossmint-hosted-checkout-v3-overlay";

export function crossmintHostedCheckoutOverlayService() {
function createOverlay(
windowClient: ReturnType<typeof PopupWindow.initSync> | ReturnType<typeof NewTabWindow.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: 15px; text-align: center; max-width: 400px; font-family: Inter; letter-spacing: -0.015em;">Complete your purchase in the 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 { NewTabWindow, 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(url: string) {
return PopupWindow.initSync(url, {
width: 450,
height: 750,
crossOrigin: true,
});
}

function createNewTabClient(url: string): ReturnType<typeof NewTabWindow.initSync> {
return NewTabWindow.initSync(url, {});
}

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

function createWindow() {
const displayType = hostedCheckoutProps.appearance?.display || "popup";
const url = getUrl(hostedCheckoutProps);

let windowClient: ReturnType<typeof PopupWindow.initSync> | ReturnType<typeof NewTabWindow.initSync>;
switch (displayType) {
case "popup":
windowClient = createPopupClient(url);
break;
case "same-tab":
windowClient = createSameTabClient(url);
break;
case "new-tab":
windowClient = createNewTabClient(url);
break;
default:
throw new Error(`Invalid display type: ${displayType}`);
}

if (hostedCheckoutProps.appearance?.overlay?.enabled !== false && displayType !== "same-tab") {
overlayService.create(windowClient);
}
}

return {
createWindow,
};
}
2 changes: 2 additions & 0 deletions packages/client/base/src/services/hosted/v3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./crossmintHostedCheckoutV3Service";
export * from "./styles/crossmintHostedCheckoutV3StylesService";
Loading
Loading