@crossmint/client-sdk-react-ui 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.d.cts +6 -4
  3. package/dist/index.d.ts +6 -4
  4. package/dist/index.js +1 -1
  5. package/package.json +8 -4
  6. package/src/components/auth/AuthForm.tsx +50 -0
  7. package/src/components/auth/AuthFormBackButton.tsx +26 -0
  8. package/src/components/auth/AuthFormDialog.tsx +33 -0
  9. package/src/components/auth/EmbeddedAuthForm.tsx +5 -0
  10. package/src/components/auth/methods/email/EmailAuthFlow.tsx +19 -0
  11. package/src/components/auth/methods/email/EmailOTPInput.tsx +123 -0
  12. package/src/components/auth/methods/email/EmailSignIn.tsx +113 -0
  13. package/src/components/auth/methods/farcaster/FarcasterSignIn.tsx +170 -0
  14. package/src/components/auth/methods/google/GoogleSignIn.tsx +62 -0
  15. package/src/components/common/Dialog.tsx +141 -0
  16. package/src/components/common/Divider.tsx +25 -0
  17. package/src/components/common/InputOTP.tsx +89 -0
  18. package/src/components/common/PoweredByCrossmint.tsx +4 -9
  19. package/src/components/common/Spinner.tsx +22 -0
  20. package/src/components/dynamic-xyz/DynamicContextProviderWrapper.tsx +12 -2
  21. package/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx +6 -1
  22. package/src/components/embed/v3/crypto/CryptoWalletConnectionHandler.tsx +11 -3
  23. package/src/components/index.ts +2 -1
  24. package/src/hooks/useAuthSignIn.ts +117 -0
  25. package/src/hooks/useOAuthWindowListener.ts +87 -0
  26. package/src/icons/alert.tsx +19 -0
  27. package/src/icons/discord.tsx +18 -0
  28. package/src/icons/emailOTP.tsx +147 -0
  29. package/src/icons/farcaster.tsx +26 -0
  30. package/src/icons/google.tsx +30 -0
  31. package/src/icons/leftArrow.tsx +20 -0
  32. package/src/icons/poweredByLeaf.tsx +2 -2
  33. package/src/providers/CrossmintAuthProvider.tsx +24 -25
  34. package/src/providers/CrossmintWalletProvider.tsx +3 -3
  35. package/src/providers/auth/AuthFormProvider.test.tsx +105 -0
  36. package/src/providers/auth/AuthFormProvider.tsx +116 -0
  37. package/src/providers/auth/FarcasterProvider.tsx +12 -0
  38. package/src/twind.config.ts +101 -1
  39. package/src/types/auth.ts +4 -0
  40. package/src/components/auth/AuthModal.tsx +0 -207
@@ -0,0 +1,123 @@
1
+ import { useState } from "react";
2
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/common/InputOTP";
3
+ import { useAuthSignIn } from "@/hooks/useAuthSignIn";
4
+ import { EmailOtpIcon } from "@/icons/emailOTP";
5
+ import { useAuthForm } from "@/providers/auth/AuthFormProvider";
6
+ import type { OtpEmailPayload } from "@/types/auth";
7
+ import { AuthFormBackButton } from "../../AuthFormBackButton";
8
+
9
+ export const EMAIL_VERIFICATION_TOKEN_LENGTH = 6;
10
+
11
+ export function EmailOTPInput({
12
+ otpEmailData,
13
+ setOtpEmailData,
14
+ }: { otpEmailData: OtpEmailPayload | null; setOtpEmailData: (data: OtpEmailPayload | null) => void }) {
15
+ const { appearance, baseUrl, apiKey, fetchAuthMaterial, setDialogOpen, setStep } = useAuthForm();
16
+ const { onConfirmEmailOtp } = useAuthSignIn();
17
+
18
+ const [token, setToken] = useState("");
19
+ const [hasError, setHasError] = useState(false);
20
+ const [loading, setLoading] = useState(false);
21
+
22
+ const handleOnSubmit = async () => {
23
+ setLoading(true);
24
+ try {
25
+ const oneTimeSecret = await onConfirmEmailOtp(
26
+ otpEmailData?.email ?? "",
27
+ otpEmailData?.emailId ?? "",
28
+ token,
29
+ {
30
+ baseUrl,
31
+ apiKey,
32
+ }
33
+ );
34
+
35
+ await fetchAuthMaterial(oneTimeSecret as string);
36
+ setDialogOpen(false);
37
+ setStep("initial");
38
+ } catch (e) {
39
+ console.error("Error signing in via email ", e);
40
+ setHasError(true);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ const handleOnBack = () => {
47
+ setStep("initial");
48
+ setOtpEmailData(null);
49
+ };
50
+
51
+ return (
52
+ <div>
53
+ <AuthFormBackButton
54
+ onClick={handleOnBack}
55
+ iconColor={appearance?.colors?.textPrimary}
56
+ ringColor={appearance?.colors?.accent}
57
+ />
58
+
59
+ <div className="flex flex-col items-center justify-start w-full">
60
+ <div className="relative left-3">
61
+ <EmailOtpIcon
62
+ customAccentColor={appearance?.colors?.accent}
63
+ customButtonBackgroundColor={appearance?.colors?.buttonBackground}
64
+ customBackgroundColor={appearance?.colors?.background}
65
+ />
66
+ </div>
67
+ <p
68
+ className="text-base font-normal mt-4 mb-1 text-center text-[#67797F]"
69
+ style={{ color: appearance?.colors?.textPrimary }}
70
+ >
71
+ {"Check your email"}
72
+ </p>
73
+ <p className="text-center px-4" style={{ color: appearance?.colors?.textSecondary }}>
74
+ {"A temporary login code has been sent to your email"}
75
+ </p>
76
+ <div className="py-8">
77
+ <InputOTP
78
+ maxLength={EMAIL_VERIFICATION_TOKEN_LENGTH}
79
+ value={token}
80
+ onChange={(val) => {
81
+ setToken(val);
82
+ setHasError(false);
83
+ }}
84
+ onComplete={handleOnSubmit}
85
+ disabled={loading}
86
+ customStyles={{
87
+ accent: appearance?.colors?.accent ?? "#04AA6D",
88
+ danger: appearance?.colors?.danger ?? "#f44336",
89
+ border: appearance?.colors?.border ?? "#E5E7EB",
90
+ textPrimary: appearance?.colors?.textPrimary ?? "#909ca3",
91
+ buttonBackground: appearance?.colors?.buttonBackground ?? "#eff6ff",
92
+ inputBackground: appearance?.colors?.inputBackground ?? "#FFFFFF",
93
+ borderRadius: appearance?.borderRadius,
94
+ }}
95
+ >
96
+ <InputOTPGroup>
97
+ <InputOTPSlot index={0} hasError={hasError} />
98
+ <InputOTPSlot index={1} hasError={hasError} />
99
+ <InputOTPSlot index={2} hasError={hasError} />
100
+ <InputOTPSlot index={3} hasError={hasError} />
101
+ <InputOTPSlot index={4} hasError={hasError} />
102
+ <InputOTPSlot index={5} hasError={hasError} />
103
+ </InputOTPGroup>
104
+ </InputOTP>
105
+ </div>
106
+
107
+ <p className="text-sm leading-tight text-center">
108
+ <span style={{ color: appearance?.colors?.textSecondary }}>
109
+ Can't find the email? Check spam folder or contact
110
+ </span>{" "}
111
+ <a
112
+ key="resend-email-link"
113
+ className="transition-opacity duration-150 text-link hover:opacity-70"
114
+ style={{ color: appearance?.colors?.textLink }}
115
+ href="mailto:support@crossmint.io"
116
+ >
117
+ support@crossmint.io
118
+ </a>
119
+ </p>
120
+ </div>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,113 @@
1
+ import { type FormEvent, useState } from "react";
2
+
3
+ import { Spinner } from "@/components/common/Spinner";
4
+ import { classNames } from "@/utils/classNames";
5
+ import { AlertIcon } from "../../../../icons/alert";
6
+ import { useAuthSignIn } from "@/hooks/useAuthSignIn";
7
+ import { isEmailValid } from "@crossmint/common-sdk-auth";
8
+ import { useAuthForm } from "@/providers/auth/AuthFormProvider";
9
+ import type { OtpEmailPayload } from "@/types/auth";
10
+
11
+ export function EmailSignIn({ setOtpEmailData }: { setOtpEmailData: (data: OtpEmailPayload) => void }) {
12
+ const { baseUrl, apiKey, appearance, setStep } = useAuthForm();
13
+ const { onEmailSignIn } = useAuthSignIn();
14
+
15
+ const [emailInput, setEmailInput] = useState("");
16
+ const [emailError, setEmailError] = useState("");
17
+ const [isLoading, setIsLoading] = useState(false);
18
+
19
+ async function handleOnSubmit(e: FormEvent<HTMLFormElement>) {
20
+ e.preventDefault();
21
+
22
+ if (!isEmailValid(emailInput)) {
23
+ setEmailError("Please enter a valid email address");
24
+ return;
25
+ }
26
+
27
+ setIsLoading(true);
28
+
29
+ try {
30
+ const trimmedEmailInput = emailInput.trim().toLowerCase();
31
+ const emailSignInRes = (await onEmailSignIn(trimmedEmailInput, { baseUrl, apiKey })) as { emailId: string };
32
+
33
+ setOtpEmailData({ email: trimmedEmailInput, emailId: emailSignInRes.emailId });
34
+ setStep("otp");
35
+ } catch (_e: unknown) {
36
+ setIsLoading(false);
37
+ setEmailError("Failed to send email. Please try again or contact support.");
38
+ }
39
+ }
40
+
41
+ return (
42
+ <>
43
+ <div className="flex flex-col items-start justify-start w-full rounded-lg">
44
+ <div className="w-full">
45
+ <p
46
+ className="text-sm font-inter font-medium text-cm-text-primary pb-2"
47
+ style={{ color: appearance?.colors?.textPrimary }}
48
+ >
49
+ Email
50
+ </p>
51
+ <form
52
+ role="form"
53
+ className="relative"
54
+ onSubmit={handleOnSubmit}
55
+ noValidate // we want to handle validation ourselves
56
+ >
57
+ <label htmlFor="emailInput" className="sr-only">
58
+ Email
59
+ </label>
60
+ <input
61
+ className={classNames(
62
+ "flex-grow text-left pl-[16px] pr-[80px] h-[58px] w-full border border-cm-border rounded-xl bg-cm-background-primary placeholder:text-sm placeholder:text-opacity-60",
63
+ "transition-all duration-200 ease-in-out", // Add smooth transition
64
+ "focus:outline-none focus:ring-1 focus:ring-opacity-50", // Add focus ring
65
+ emailError ? "border-red-500" : ""
66
+ )}
67
+ style={{
68
+ color: appearance?.colors?.textPrimary,
69
+ borderRadius: appearance?.borderRadius,
70
+ borderColor: emailError ? appearance?.colors?.danger : appearance?.colors?.border,
71
+ backgroundColor: appearance?.colors?.inputBackground,
72
+ // @ts-expect-error --tw-ring-color is not recognized by typescript but gets picked up by tailwind
73
+ "--tw-ring-color": appearance?.colors?.accent ?? "#1A73E8",
74
+ }}
75
+ type="email"
76
+ placeholder={"Enter email"}
77
+ value={emailInput}
78
+ onChange={(e) => {
79
+ setEmailInput(e.target.value);
80
+ setEmailError("");
81
+ }}
82
+ readOnly={isLoading}
83
+ // aria-invalid={emailError != null}
84
+ aria-describedby="emailError"
85
+ />
86
+ <div className="absolute inset-y-0 right-0 flex items-center pr-4">
87
+ {emailError && <AlertIcon customColor={appearance?.colors?.danger} />}
88
+ {isLoading && (
89
+ <Spinner
90
+ style={{
91
+ color: appearance?.colors?.textSecondary,
92
+ fill: appearance?.colors?.textPrimary,
93
+ }}
94
+ />
95
+ )}
96
+ {!emailError && !isLoading && (
97
+ <button
98
+ type="submit"
99
+ className={classNames("cursor-pointer text-nowrap")}
100
+ style={{ color: appearance?.colors?.accent ?? "#1A73E8" }}
101
+ disabled={!emailInput}
102
+ >
103
+ Sign in
104
+ </button>
105
+ )}
106
+ </div>
107
+ </form>
108
+ {emailError && <p className="text-xs text-red-500 mb-2 pt-2">{emailError}</p>}
109
+ </div>
110
+ </div>
111
+ </>
112
+ );
113
+ }
@@ -0,0 +1,170 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { useSignIn, QRCode, type UseSignInData } from "@farcaster/auth-kit";
3
+ import { useAuthSignIn } from "@/hooks/useAuthSignIn";
4
+ import { FarcasterIcon } from "@/icons/farcaster";
5
+ import { useAuthForm } from "@/providers/auth/AuthFormProvider";
6
+ import { Spinner } from "@/components/common/Spinner";
7
+ import { classNames } from "@/utils/classNames";
8
+ import { AuthFormBackButton } from "../../AuthFormBackButton";
9
+
10
+ export function FarcasterSignIn() {
11
+ const { step, appearance, setStep } = useAuthForm();
12
+
13
+ if (step === "initial") {
14
+ return (
15
+ <div>
16
+ <button
17
+ className={classNames(
18
+ "relative flex text-base p-4 bg-cm-muted-primary text-cm-text-primary border border-cm-border items-center w-full rounded-xl justify-center",
19
+ "transition-all duration-200 ease-in-out",
20
+ "focus:outline-none focus:ring-1 focus:ring-opacity-50"
21
+ )}
22
+ style={{
23
+ borderRadius: appearance?.borderRadius,
24
+ borderColor: appearance?.colors?.border,
25
+ backgroundColor: appearance?.colors?.buttonBackground,
26
+ // @ts-expect-error --tw-ring-color is not recognized by typescript but gets picked up by tailwind
27
+ "--tw-ring-color": appearance?.colors?.accent ?? "#1A73E8",
28
+ }}
29
+ onClick={() => {
30
+ setStep("qrCode");
31
+ }}
32
+ >
33
+ <FarcasterIcon className="h-[25px] w-[25px] absolute left-[18px]" />
34
+ <span
35
+ className="font-medium"
36
+ style={{ margin: "0px 32px", color: appearance?.colors?.textPrimary }}
37
+ >
38
+ Farcaster
39
+ </span>
40
+ <span className="sr-only">Sign in with Farcaster</span>
41
+ </button>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ if (step === "qrCode") {
47
+ return <FarcasterQRCode />;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ // We want this to be a separate component so it can completely un-render when the user goes back to the initial screen
54
+ function FarcasterQRCode() {
55
+ const { appearance, baseUrl, apiKey, setStep, fetchAuthMaterial, setDialogOpen } = useAuthForm();
56
+ const { onFarcasterSignIn } = useAuthSignIn();
57
+ const [farcasterData, setFarcasterData] = useState<UseSignInData | null>(null);
58
+
59
+ const farcasterProps = useMemo(
60
+ () => ({
61
+ onSuccess: (data: UseSignInData) => {
62
+ // Step 3. set the farcaster data once the sign in is successful
63
+ setFarcasterData(data);
64
+ },
65
+ }),
66
+ []
67
+ );
68
+
69
+ const { signIn, url: qrCodeUrl, connect, signOut, isConnected } = useSignIn(farcasterProps);
70
+
71
+ const handleFarcasterSignIn = async (data: UseSignInData) => {
72
+ try {
73
+ const oneTimeSecret = await onFarcasterSignIn(data, { baseUrl, apiKey });
74
+ // Step 5. fetch the auth material, close the dialog, and unrender any farcaster client stuff
75
+ await fetchAuthMaterial(oneTimeSecret as string);
76
+ setDialogOpen(false);
77
+ setStep("initial");
78
+ } catch (error) {
79
+ console.error("Error during Farcaster sign-in:", error);
80
+ }
81
+ };
82
+
83
+ useEffect(() => {
84
+ if (farcasterData != null) {
85
+ // Step 4. call the handleFarcasterSignInfunction to handle the sign in
86
+ handleFarcasterSignIn(farcasterData);
87
+ }
88
+ }, [farcasterData]);
89
+
90
+ useEffect(() => {
91
+ if (isConnected) {
92
+ // Step 2. once connected, call the signIn function to start the sign in process
93
+ signIn();
94
+ }
95
+ }, [isConnected]);
96
+
97
+ useEffect(() => {
98
+ // Step 1. call the connect function to initialize the connection
99
+ connect();
100
+ }, []);
101
+
102
+ return (
103
+ <div>
104
+ <AuthFormBackButton
105
+ onClick={() => {
106
+ signOut();
107
+ setStep("initial");
108
+ }}
109
+ iconColor={appearance?.colors?.textPrimary}
110
+ ringColor={appearance?.colors?.accent}
111
+ />
112
+
113
+ <div className="flex flex-col items-center gap-4">
114
+ <div className="text-center">
115
+ <h3
116
+ className="text-lg font-semibold text-cm-text-primary mb-2"
117
+ style={{ color: appearance?.colors?.textPrimary }}
118
+ >
119
+ Sign in with Farcaster
120
+ </h3>
121
+ <p
122
+ className="text-base font-normal text-cm-text-secondary"
123
+ style={{ color: appearance?.colors?.textSecondary }}
124
+ >
125
+ Scan with your phone's camera to continue.
126
+ </p>
127
+ </div>
128
+ <div
129
+ className="bg-white aspect-square rounded-lg p-4"
130
+ style={{
131
+ backgroundColor: appearance?.colors?.inputBackground,
132
+ borderRadius: appearance?.borderRadius,
133
+ }}
134
+ >
135
+ {qrCodeUrl != null ? (
136
+ <QRCode uri={qrCodeUrl} size={280} />
137
+ ) : (
138
+ <div className="min-h-[246px] flex items-center justify-center">
139
+ <Spinner
140
+ style={{
141
+ color: appearance?.colors?.textSecondary,
142
+ fill: appearance?.colors?.textPrimary,
143
+ }}
144
+ />
145
+ </div>
146
+ )}
147
+ </div>
148
+ {qrCodeUrl ? (
149
+ <>
150
+ <p
151
+ className="text-base text-center font-normal text-cm-text-secondary"
152
+ style={{ color: appearance?.colors?.textSecondary }}
153
+ >
154
+ Alternatively, click on this link to open Warpcast.
155
+ </p>
156
+ <a
157
+ href={qrCodeUrl}
158
+ rel="noopener noreferrer"
159
+ target="_blank"
160
+ className="text-base font-normal text-cm-ring"
161
+ style={{ color: appearance?.colors?.textLink }}
162
+ >
163
+ Open Warpcast
164
+ </a>
165
+ </>
166
+ ) : null}
167
+ </div>
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,62 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { GoogleIcon } from "@/icons/google";
3
+ import { useOAuthWindowListener } from "@/hooks/useOAuthWindowListener";
4
+ import { Spinner } from "@/components/common/Spinner";
5
+ import { useAuthForm } from "@/providers/auth/AuthFormProvider";
6
+ import { classNames } from "@/utils/classNames";
7
+
8
+ export function GoogleSignIn({ ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
9
+ const { step, apiKey, baseUrl, appearance, isLoadingOauthUrlMap, fetchAuthMaterial } = useAuthForm();
10
+ const { createPopupAndSetupListeners, isLoading: isLoadingOAuthWindow } = useOAuthWindowListener("google", {
11
+ apiKey,
12
+ baseUrl,
13
+ fetchAuthMaterial,
14
+ });
15
+ const isLoading = isLoadingOauthUrlMap || isLoadingOAuthWindow;
16
+
17
+ if (step !== "initial") {
18
+ return null;
19
+ }
20
+
21
+ return (
22
+ <button
23
+ className={classNames(
24
+ "relative flex text-base p-4 bg-cm-muted-primary text-cm-text-primary border border-cm-border items-center w-full rounded-xl justify-center",
25
+ "transition-all duration-200 ease-in-out", // Add smooth transition
26
+ "focus:outline-none focus:ring-1 focus:ring-opacity-50", // Add focus ring
27
+ isLoading ? "cursor-not-allowed" : ""
28
+ )}
29
+ style={{
30
+ borderRadius: appearance?.borderRadius,
31
+ borderColor: appearance?.colors?.border,
32
+ backgroundColor: appearance?.colors?.buttonBackground,
33
+ // @ts-expect-error --tw-ring-color is not recognized by typescript but gets picked up by tailwind
34
+ "--tw-ring-color": appearance?.colors?.accent ?? "#1A73E8",
35
+ }}
36
+ onClick={isLoading ? undefined : createPopupAndSetupListeners}
37
+ {...props}
38
+ >
39
+ <>
40
+ <GoogleIcon className="h-[25px] w-[25px] absolute left-[18px]" />
41
+ {isLoading ? (
42
+ <Spinner
43
+ style={{
44
+ color: appearance?.colors?.textSecondary,
45
+ fill: appearance?.colors?.textPrimary,
46
+ }}
47
+ />
48
+ ) : (
49
+ <span
50
+ className="font-medium"
51
+ style={{ margin: "0px 32px", color: appearance?.colors?.textPrimary }}
52
+ >
53
+ Google
54
+ </span>
55
+ )}
56
+ </>
57
+
58
+ {/* For accessibility sake */}
59
+ <span className="sr-only">Sign in with Google</span>
60
+ </button>
61
+ );
62
+ }
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { classNames } from "@/utils/classNames";
6
+
7
+ const Dialog = DialogPrimitive.Root;
8
+ const DialogTrigger = DialogPrimitive.Trigger;
9
+ const DialogClose = DialogPrimitive.Close;
10
+ const DialogPortal = DialogPrimitive.Portal;
11
+
12
+ const DialogOverlay = React.forwardRef<
13
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
14
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
15
+ >(({ className, ...props }, ref) => (
16
+ <DialogPrimitive.Overlay
17
+ ref={ref}
18
+ className={classNames(
19
+ "fixed inset-0 z-50 bg-black/80 backdrop-blur-[2px] data-[state=closed]:animate-out data-[state=closed]:animate-fade-out data-[state=open]:animate-in data-[state=open]:animate-fade-in",
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ ));
25
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
26
+
27
+ interface DialogContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
28
+ showCloseButton?: boolean;
29
+ closeButtonColor?: string;
30
+ closeButtonRingColor?: string;
31
+ }
32
+
33
+ const DialogContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, DialogContentProps>(
34
+ ({ className, children, showCloseButton = true, closeButtonColor, closeButtonRingColor, ...props }, ref) => (
35
+ <DialogPortal>
36
+ <DialogOverlay />
37
+ <DialogPrimitive.Content
38
+ ref={ref}
39
+ className={classNames(
40
+ "fixed z-50 p-6 pb-2 bg-cm-background-primary border border-cm-border shadow-xl transition-none",
41
+ // Small viewport styles (bottom sheet)
42
+ "inset-x-0 bottom-0 w-full border-t rounded-t-xl",
43
+ "data-[state=closed]:animate-slide-out-to-bottom data-[state=open]:animate-slide-in-from-bottom",
44
+ // Regular viewport styles (centered modal)
45
+ "xs:inset-auto !xs:p-10 xs:left-[50%] xs:top-[50%] xs:translate-x-[-50%] xs:translate-y-[-50%]",
46
+ "xs:max-w-[448px] xs:rounded-xl",
47
+ "xs:data-[state=closed]:animate-fade-out xs:data-[state=closed]:animate-zoom-out-95",
48
+ "xs:data-[state=open]:animate-fade-in xs:data-[state=open]:animate-zoom-in-95",
49
+ // Duration for animations
50
+ "data-[state=closed]:duration-300 data-[state=open]:duration-500",
51
+ className
52
+ )}
53
+ {...props}
54
+ >
55
+ {children}
56
+ {showCloseButton && (
57
+ <DialogPrimitive.Close
58
+ className={classNames(
59
+ "absolute rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100",
60
+ "focus:outline-none focus:ring-2 focus:ring-cm-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-text-primary",
61
+ "right-4 top-4 !xs:right-6 !xs:top-6"
62
+ )}
63
+ style={{
64
+ color: closeButtonColor,
65
+ // @ts-expect-error --tw-ring-color is not recognized by typescript but gets picked up by tailwind
66
+ "--tw-ring-color": closeButtonRingColor ?? "#1A73E8",
67
+ }}
68
+ >
69
+ <svg
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ width="24"
72
+ height="24"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ strokeWidth="2"
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ className="h-6 w-6"
80
+ >
81
+ <path d="M18 6 6 18" />
82
+ <path d="m6 6 12 12" />
83
+ </svg>
84
+ <span className="sr-only">Close</span>
85
+ </DialogPrimitive.Close>
86
+ )}
87
+ </DialogPrimitive.Content>
88
+ </DialogPortal>
89
+ )
90
+ );
91
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
92
+
93
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
94
+ <div className={classNames("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
95
+ );
96
+ DialogHeader.displayName = "DialogHeader";
97
+
98
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
99
+ <div
100
+ className={classNames("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
101
+ {...props}
102
+ />
103
+ );
104
+ DialogFooter.displayName = "DialogFooter";
105
+
106
+ const DialogTitle = React.forwardRef<
107
+ React.ElementRef<typeof DialogPrimitive.Title>,
108
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
109
+ >(({ className, ...props }, ref) => (
110
+ <DialogPrimitive.Title
111
+ ref={ref}
112
+ className={classNames("text-lg font-semibold leading-none tracking-tight", className)}
113
+ {...props}
114
+ />
115
+ ));
116
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
117
+
118
+ const DialogDescription = React.forwardRef<
119
+ React.ElementRef<typeof DialogPrimitive.Description>,
120
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
121
+ >(({ className, ...props }, ref) => (
122
+ <DialogPrimitive.Description
123
+ ref={ref}
124
+ className={classNames("text-sm text-muted-foreground", className)}
125
+ {...props}
126
+ />
127
+ ));
128
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
129
+
130
+ export {
131
+ Dialog,
132
+ DialogPortal,
133
+ DialogOverlay,
134
+ DialogClose,
135
+ DialogTrigger,
136
+ DialogContent,
137
+ DialogHeader,
138
+ DialogFooter,
139
+ DialogTitle,
140
+ DialogDescription,
141
+ };
@@ -0,0 +1,25 @@
1
+ import { useAuthForm } from "@/providers/auth/AuthFormProvider";
2
+ import type { UIConfig } from "@crossmint/common-sdk-base";
3
+
4
+ export function Divider({ appearance, text }: { appearance?: UIConfig; text?: string }) {
5
+ const { step } = useAuthForm();
6
+
7
+ if (step !== "initial") {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ <div className="flex items-center justify-center w-full pt-1 pb-2">
13
+ <span className="w-full h-[1px] bg-cm-border" style={{ backgroundColor: appearance?.colors?.border }} />
14
+ {text != null ? (
15
+ <p
16
+ className="flex-none px-2 text-sm text-cm-text-primary"
17
+ style={{ color: appearance?.colors?.textSecondary }}
18
+ >
19
+ {text}
20
+ </p>
21
+ ) : null}
22
+ <span className="w-full h-[1px] bg-cm-border" style={{ backgroundColor: appearance?.colors?.border }} />
23
+ </div>
24
+ );
25
+ }