@crossmint/client-sdk-react-ui 1.7.1 → 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.
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +20 -7
- package/dist/index.d.ts +20 -7
- package/dist/index.js +1 -1
- package/package.json +17 -7
- package/src/components/auth/AuthForm.tsx +50 -0
- package/src/components/auth/AuthFormBackButton.tsx +26 -0
- package/src/components/auth/AuthFormDialog.tsx +33 -0
- package/src/components/auth/EmbeddedAuthForm.tsx +5 -0
- package/src/components/auth/methods/email/EmailAuthFlow.tsx +19 -0
- package/src/components/auth/methods/email/EmailOTPInput.tsx +123 -0
- package/src/components/auth/methods/email/EmailSignIn.tsx +113 -0
- package/src/components/auth/methods/farcaster/FarcasterSignIn.tsx +170 -0
- package/src/components/auth/methods/google/GoogleSignIn.tsx +62 -0
- package/src/components/common/Dialog.tsx +141 -0
- package/src/components/common/Divider.tsx +25 -0
- package/src/components/common/InputOTP.tsx +89 -0
- package/src/components/common/PoweredByCrossmint.tsx +4 -9
- package/src/components/common/Spinner.tsx +22 -0
- package/src/components/dynamic-xyz/DynamicContextProviderWrapper.tsx +31 -0
- package/src/components/embed/v3/CrossmintEmbeddedCheckoutV3.tsx +7 -0
- package/src/components/embed/v3/EmbeddedCheckoutV3IFrame.tsx +74 -0
- package/src/components/embed/v3/crypto/CryptoWalletConnectionHandler.tsx +138 -0
- package/src/components/embed/v3/crypto/utils/handleEvmTransaction.ts +65 -0
- package/src/components/embed/v3/crypto/utils/handleSendTransaction.ts +31 -0
- package/src/components/embed/v3/crypto/utils/handleSolanaTransaction.ts +51 -0
- package/src/components/embed/v3/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAuthSignIn.ts +117 -0
- package/src/hooks/useCrossmintCheckout.tsx +54 -0
- package/src/hooks/useOAuthWindowListener.ts +87 -0
- package/src/hooks/useRefreshToken.test.ts +21 -8
- package/src/hooks/useRefreshToken.ts +5 -4
- package/src/icons/alert.tsx +19 -0
- package/src/icons/discord.tsx +18 -0
- package/src/icons/emailOTP.tsx +147 -0
- package/src/icons/farcaster.tsx +26 -0
- package/src/icons/google.tsx +30 -0
- package/src/icons/leftArrow.tsx +20 -0
- package/src/icons/poweredByLeaf.tsx +2 -2
- package/src/providers/CrossmintAuthProvider.test.tsx +4 -3
- package/src/providers/CrossmintAuthProvider.tsx +36 -32
- package/src/providers/CrossmintWalletProvider.tsx +3 -3
- package/src/providers/auth/AuthFormProvider.test.tsx +105 -0
- package/src/providers/auth/AuthFormProvider.tsx +116 -0
- package/src/providers/auth/FarcasterProvider.tsx +12 -0
- package/src/twind.config.ts +101 -1
- package/src/types/auth.ts +4 -0
- package/src/utils/authCookies.ts +0 -3
- package/src/utils/createCrossmintApiClient.ts +17 -0
- 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
|
+
}
|