@pradip1995/segment-login-template 0.2.5 → 0.4.0
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/package.json +9 -2
- package/src/account-addresses.tsx +216 -0
- package/src/account-guest-orders.tsx +60 -0
- package/src/account-orders.tsx +81 -0
- package/src/account-overview.tsx +111 -0
- package/src/account-payment-methods.tsx +229 -0
- package/src/account-profile.tsx +198 -0
- package/src/account-utils.ts +20 -0
- package/src/auth-server.ts +307 -0
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +86 -0
- package/src/index.ts +5 -0
- package/src/login-form.tsx +411 -0
- package/src/otp-component.ts +14 -0
- package/src/otp-input.tsx +46 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +324 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +81 -200
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
checkEmailRegistered,
|
|
5
|
+
completePasswordReset,
|
|
6
|
+
registerCustomer as registerCustomerBase,
|
|
7
|
+
requestPasswordReset,
|
|
8
|
+
sendCustomerOTP,
|
|
9
|
+
verifyCustomerOTP,
|
|
10
|
+
} from "@pradip1995/commerce-core/data/customer-registration"
|
|
11
|
+
import { transferCart } from "@pradip1995/commerce-core/data/customer"
|
|
12
|
+
import { getCacheTag, setAuthToken } from "@pradip1995/commerce-core/data/cookies"
|
|
13
|
+
import { sendOTP, verifyOTP } from "@pradip1995/commerce-core/data/guest"
|
|
14
|
+
import { getGoogleOAuthCallbackUrl } from "@pradip1995/commerce-core/util/google-oauth"
|
|
15
|
+
import { revalidateTag } from "next/cache"
|
|
16
|
+
import { redirect } from "next/navigation"
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
sendCustomerOTP,
|
|
20
|
+
verifyCustomerOTP,
|
|
21
|
+
requestPasswordReset,
|
|
22
|
+
checkEmailRegistered,
|
|
23
|
+
completePasswordReset,
|
|
24
|
+
sendOTP,
|
|
25
|
+
verifyOTP,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRegistrationError(message: string): string {
|
|
29
|
+
const lower = message.toLowerCase()
|
|
30
|
+
if (
|
|
31
|
+
lower.includes("identity with email already exists") ||
|
|
32
|
+
lower.includes("email already exists") ||
|
|
33
|
+
lower.includes("email is already registered")
|
|
34
|
+
) {
|
|
35
|
+
return "Email already exists"
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
lower.includes("identity with phone already exists") ||
|
|
39
|
+
lower.includes("phone already exists")
|
|
40
|
+
) {
|
|
41
|
+
return "Phone number already exists"
|
|
42
|
+
}
|
|
43
|
+
return message
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function registerCustomer(data: {
|
|
47
|
+
email: string
|
|
48
|
+
first_name: string
|
|
49
|
+
last_name: string
|
|
50
|
+
phone?: string
|
|
51
|
+
password: string
|
|
52
|
+
}) {
|
|
53
|
+
const result = await registerCustomerBase(data)
|
|
54
|
+
if (!result.success && result.error) {
|
|
55
|
+
return { ...result, error: normalizeRegistrationError(result.error) }
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getHeaders() {
|
|
61
|
+
return {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "",
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getBaseUrl() {
|
|
68
|
+
return process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function sendAuthOtp(input: {
|
|
72
|
+
email?: string
|
|
73
|
+
phone?: string
|
|
74
|
+
type: "email_auth" | "phone_auth"
|
|
75
|
+
}) {
|
|
76
|
+
const baseUrl = getBaseUrl()
|
|
77
|
+
const body =
|
|
78
|
+
input.type === "email_auth"
|
|
79
|
+
? { email: input.email?.toLowerCase().trim(), type: input.type }
|
|
80
|
+
: { phone: input.phone?.replace(/[^\d+]/g, ""), type: input.type }
|
|
81
|
+
|
|
82
|
+
const response = await fetch(`${baseUrl}/store/customers/otp/send`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: getHeaders(),
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const err = await response.json().catch(() => ({}))
|
|
90
|
+
return {
|
|
91
|
+
success: false as const,
|
|
92
|
+
error: (err as { message?: string }).message || "Failed to send verification code",
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data = (await response.json()) as {
|
|
97
|
+
token?: string
|
|
98
|
+
expires_at?: string
|
|
99
|
+
is_new_user?: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true as const,
|
|
104
|
+
token: data.token,
|
|
105
|
+
isNewUser: data.is_new_user ?? false,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function verifyAuthOtpAndLogin(input: {
|
|
110
|
+
token: string
|
|
111
|
+
code: string
|
|
112
|
+
countryCode: string
|
|
113
|
+
first_name?: string
|
|
114
|
+
last_name?: string
|
|
115
|
+
}) {
|
|
116
|
+
const baseUrl = getBaseUrl()
|
|
117
|
+
|
|
118
|
+
const response = await fetch(`${baseUrl}/store/customers/otp/verify`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: getHeaders(),
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
token: input.token,
|
|
123
|
+
code: input.code,
|
|
124
|
+
first_name: input.first_name,
|
|
125
|
+
last_name: input.last_name,
|
|
126
|
+
}),
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const err = await response.json().catch(() => ({}))
|
|
131
|
+
return {
|
|
132
|
+
success: false as const,
|
|
133
|
+
error: (err as { message?: string }).message || "Invalid verification code",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = (await response.json()) as {
|
|
138
|
+
token?: string | null
|
|
139
|
+
verified?: boolean
|
|
140
|
+
is_new_user?: boolean
|
|
141
|
+
customer?: { email?: string }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const jwt = data.token
|
|
145
|
+
if (!jwt) {
|
|
146
|
+
return {
|
|
147
|
+
success: false as const,
|
|
148
|
+
error: "Verification succeeded but login token was not returned. Try signing in with your password.",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await setAuthToken(jwt)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await transferCart(jwt)
|
|
156
|
+
} catch {
|
|
157
|
+
// non-fatal
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
161
|
+
revalidateTag(customerCacheTag)
|
|
162
|
+
|
|
163
|
+
const countryCode = input.countryCode || "in"
|
|
164
|
+
redirect(`/${countryCode}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function completePasswordResetAction(input: {
|
|
168
|
+
email: string
|
|
169
|
+
password: string
|
|
170
|
+
token: string
|
|
171
|
+
countryCode: string
|
|
172
|
+
}) {
|
|
173
|
+
const result = await completePasswordReset({
|
|
174
|
+
email: input.email,
|
|
175
|
+
password: input.password,
|
|
176
|
+
token: input.token,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
return result
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
redirect(`/${input.countryCode || "in"}/account`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function verifyRegistrationOtp(input: {
|
|
187
|
+
otpToken: string
|
|
188
|
+
code: string
|
|
189
|
+
countryCode: string
|
|
190
|
+
}) {
|
|
191
|
+
const response = await fetch(`${getBaseUrl()}/store/customers/otp/verify`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: getHeaders(),
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
token: input.otpToken,
|
|
196
|
+
code: input.code,
|
|
197
|
+
}),
|
|
198
|
+
cache: "no-store",
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
202
|
+
message?: string
|
|
203
|
+
token?: string | null
|
|
204
|
+
email_verified?: boolean
|
|
205
|
+
phone_verified?: boolean
|
|
206
|
+
needs_login?: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
return {
|
|
211
|
+
success: false as const,
|
|
212
|
+
error: data.message || "Invalid verification code",
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const loginToken = data.token
|
|
217
|
+
if (loginToken) {
|
|
218
|
+
await setAuthToken(loginToken)
|
|
219
|
+
try {
|
|
220
|
+
await transferCart(loginToken)
|
|
221
|
+
} catch {
|
|
222
|
+
// non-fatal
|
|
223
|
+
}
|
|
224
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
225
|
+
revalidateTag(customerCacheTag)
|
|
226
|
+
redirect(`/${input.countryCode || "in"}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
success: true as const,
|
|
231
|
+
emailVerified: data.email_verified,
|
|
232
|
+
phoneVerified: data.phone_verified,
|
|
233
|
+
needsLogin: data.needs_login ?? true,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function sendRegistrationOtp(
|
|
238
|
+
customerId: string,
|
|
239
|
+
type: "email_verification" | "phone_verification"
|
|
240
|
+
) {
|
|
241
|
+
const result = await sendCustomerOTP(customerId, type)
|
|
242
|
+
if (result.success) {
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const message = result.error || "Failed to send verification code"
|
|
247
|
+
if (message.includes("Channel configuration not found")) {
|
|
248
|
+
return {
|
|
249
|
+
success: false as const,
|
|
250
|
+
error:
|
|
251
|
+
"Verification email is not configured on the backend. Rebuild and restart the Medusa server with customer-registration OTP settings.",
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { success: false as const, error: message }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function initiateGoogleAuth(countryCode?: string) {
|
|
259
|
+
try {
|
|
260
|
+
const backendUrl = getBaseUrl()
|
|
261
|
+
|
|
262
|
+
if (!backendUrl) {
|
|
263
|
+
return { error: "Backend URL not configured" }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!process.env.GOOGLE_CLIENT_ID && !process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
|
|
267
|
+
return { error: "Google sign-in is not configured on the storefront" }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const callbackUrl = getGoogleOAuthCallbackUrl(countryCode)
|
|
271
|
+
|
|
272
|
+
const response = await fetch(`${backendUrl}/auth/customer/google`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ callback_url: callbackUrl }),
|
|
276
|
+
cache: "no-store",
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
const err = (await response.json().catch(() => ({}))) as {
|
|
281
|
+
message?: string
|
|
282
|
+
type?: string
|
|
283
|
+
}
|
|
284
|
+
const message = err.message?.trim()
|
|
285
|
+
if (message?.includes("Unable to retrieve the auth provider")) {
|
|
286
|
+
return {
|
|
287
|
+
error:
|
|
288
|
+
"Google sign-in is not enabled on the backend. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in backend/.env and restart the Medusa server.",
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { error: message || "Google sign-in failed. Check backend Google OAuth configuration." }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result = await response.json()
|
|
295
|
+
const redirectUrl =
|
|
296
|
+
typeof result === "string" ? result : (result as { location?: string })?.location
|
|
297
|
+
|
|
298
|
+
if (!redirectUrl) {
|
|
299
|
+
return { error: "No redirect URL received from Google OAuth" }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { redirectUrl }
|
|
303
|
+
} catch (error: unknown) {
|
|
304
|
+
const message = error instanceof Error ? error.message : "Internal server error"
|
|
305
|
+
return { error: message }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import OtpInput from "@pradip1995/commerce-auth/components/otp-input"
|
|
4
|
+
import type { OtpVerificationModalProps } from "./otp-verification-modal"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OTP modal styled like commerce-auth/chocomelon, compatible with segment-login-template props.
|
|
8
|
+
* Use in pages.config: `"otpComponent": "@pradip1995/segment-login-template/commerce-auth-otp-modal"`
|
|
9
|
+
*/
|
|
10
|
+
export default function CommerceAuthOtpModal({
|
|
11
|
+
open,
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
otp,
|
|
15
|
+
onOtpChange,
|
|
16
|
+
error,
|
|
17
|
+
pending,
|
|
18
|
+
success = false,
|
|
19
|
+
successTitle = "Verified",
|
|
20
|
+
successMessage = "Verification successful!",
|
|
21
|
+
verifyLabel = "Verify",
|
|
22
|
+
onVerify,
|
|
23
|
+
onResend,
|
|
24
|
+
onClose,
|
|
25
|
+
preventClose = false,
|
|
26
|
+
extraContent,
|
|
27
|
+
}: OtpVerificationModalProps) {
|
|
28
|
+
if (!open) return null
|
|
29
|
+
|
|
30
|
+
function handleBackdropClick() {
|
|
31
|
+
if (preventClose || pending || success) return
|
|
32
|
+
onClose?.()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
className="absolute inset-0 bg-black/40"
|
|
40
|
+
aria-label="Close verification dialog"
|
|
41
|
+
onClick={handleBackdropClick}
|
|
42
|
+
/>
|
|
43
|
+
<div className="relative w-full max-w-md rounded-2xl bg-page-bg shadow-xl p-6">
|
|
44
|
+
<div className="flex flex-col gap-6 py-4 px-2">
|
|
45
|
+
<div className="text-center">
|
|
46
|
+
<h2 className="text-2xl font-bold text-heading mb-2">
|
|
47
|
+
{success ? successTitle : title}
|
|
48
|
+
</h2>
|
|
49
|
+
{!success && (
|
|
50
|
+
<p className="text-sm text-gray-500 leading-relaxed max-w-[320px] mx-auto">
|
|
51
|
+
{description}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{success ? (
|
|
57
|
+
<div className="text-center py-6">
|
|
58
|
+
<p className="text-brand-accent font-bold animate-pulse">{successMessage}</p>
|
|
59
|
+
</div>
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
{extraContent}
|
|
63
|
+
|
|
64
|
+
<OtpInput value={otp} onChange={onOtpChange} autoFocus />
|
|
65
|
+
|
|
66
|
+
{error && (
|
|
67
|
+
<p className="text-red-500 text-xs text-center" role="alert">
|
|
68
|
+
{error}
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<div className="space-y-3">
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={onVerify}
|
|
76
|
+
disabled={pending || otp.length < 6}
|
|
77
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:opacity-90 rounded-[30px] transition-colors disabled:opacity-60"
|
|
78
|
+
>
|
|
79
|
+
{pending ? "Verifying…" : verifyLabel}
|
|
80
|
+
</button>
|
|
81
|
+
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={onResend}
|
|
85
|
+
disabled={pending}
|
|
86
|
+
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium disabled:opacity-60"
|
|
87
|
+
>
|
|
88
|
+
Resend code
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{preventClose && (
|
|
93
|
+
<p className="text-xs text-muted text-center">
|
|
94
|
+
Verification is required to complete{" "}
|
|
95
|
+
{verifyLabel.toLowerCase().includes("sign in") ? "sign in" : "registration"}.
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { checkEmailRegistered, requestPasswordReset } from "./auth-server"
|
|
5
|
+
|
|
6
|
+
export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|
7
|
+
const [email, setEmail] = useState("")
|
|
8
|
+
const [message, setMessage] = useState<string | null>(null)
|
|
9
|
+
const [error, setError] = useState<string | null>(null)
|
|
10
|
+
const [pending, setPending] = useState(false)
|
|
11
|
+
const [success, setSuccess] = useState(false)
|
|
12
|
+
|
|
13
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
14
|
+
e.preventDefault()
|
|
15
|
+
if (!email.trim()) return
|
|
16
|
+
|
|
17
|
+
setPending(true)
|
|
18
|
+
setMessage(null)
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const check = await checkEmailRegistered(email.trim())
|
|
23
|
+
if (!check.exists) {
|
|
24
|
+
setError("This email is not registered. Please create a new account instead.")
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await requestPasswordReset(email.trim())
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
setError(result.error || "Failed to send reset link")
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setSuccess(true)
|
|
35
|
+
setMessage(result.message || "If an account exists, you will receive a reset link shortly.")
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err.message : "Something went wrong")
|
|
38
|
+
} finally {
|
|
39
|
+
setPending(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (success) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-4 text-center py-4">
|
|
46
|
+
<div className="w-14 h-14 mx-auto rounded-full bg-green-100 flex items-center justify-center text-2xl">
|
|
47
|
+
✉
|
|
48
|
+
</div>
|
|
49
|
+
<h2 className="section-heading text-lg">Check your email</h2>
|
|
50
|
+
<p className="text-sm text-muted">{message}</p>
|
|
51
|
+
<button type="button" onClick={onBack} className="btn-outline w-full">
|
|
52
|
+
Back to sign in
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
60
|
+
<p className="text-sm text-muted">
|
|
61
|
+
Enter your email and we'll send a link to reset your password.
|
|
62
|
+
</p>
|
|
63
|
+
<label className="block">
|
|
64
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
65
|
+
Email
|
|
66
|
+
</span>
|
|
67
|
+
<input
|
|
68
|
+
name="email"
|
|
69
|
+
type="email"
|
|
70
|
+
value={email}
|
|
71
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
72
|
+
required
|
|
73
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
74
|
+
/>
|
|
75
|
+
</label>
|
|
76
|
+
{error && <p className="text-sm text-brand-sale">{error}</p>}
|
|
77
|
+
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
78
|
+
{pending ? "Sending…" : "Send reset link"}
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={onBack}
|
|
83
|
+
className="text-sm text-muted hover:text-brand-accent w-full text-center"
|
|
84
|
+
>
|
|
85
|
+
Back to sign in
|
|
86
|
+
</button>
|
|
87
|
+
</form>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
clearMedusaAuthCookies,
|
|
6
|
+
GOOGLE_LOGIN_COUNTRY_CODE_KEY,
|
|
7
|
+
} from "@pradip1995/commerce-auth/util/google-auth-client"
|
|
8
|
+
import { initiateGoogleAuth } from "./auth-server"
|
|
9
|
+
|
|
10
|
+
const BUTTON_CLASS =
|
|
11
|
+
"btn-outline w-full flex items-center justify-center gap-2.5 py-3 px-4"
|
|
12
|
+
|
|
13
|
+
export default function GoogleAuthSection({ countryCode }: { countryCode: string }) {
|
|
14
|
+
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
|
|
15
|
+
const [loading, setLoading] = useState(false)
|
|
16
|
+
const [error, setError] = useState<string | null>(null)
|
|
17
|
+
|
|
18
|
+
if (!clientId) return null
|
|
19
|
+
|
|
20
|
+
const handleClick = async () => {
|
|
21
|
+
setError(null)
|
|
22
|
+
setLoading(true)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY, countryCode)
|
|
26
|
+
clearMedusaAuthCookies()
|
|
27
|
+
|
|
28
|
+
const result = await initiateGoogleAuth(countryCode)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
setError(result.error)
|
|
32
|
+
setLoading(false)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (result.redirectUrl) {
|
|
37
|
+
window.location.href = result.redirectUrl
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setLoading(false)
|
|
42
|
+
} catch {
|
|
43
|
+
setError("Google sign-in failed. Please try again.")
|
|
44
|
+
setLoading(false)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="mb-6">
|
|
50
|
+
{error && (
|
|
51
|
+
<p className="mb-3 text-sm text-red-600" role="alert">
|
|
52
|
+
{error}
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
disabled={loading}
|
|
59
|
+
className={BUTTON_CLASS}
|
|
60
|
+
style={{ borderRadius: "30px" }}
|
|
61
|
+
>
|
|
62
|
+
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
|
63
|
+
<path
|
|
64
|
+
fill="#4285F4"
|
|
65
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
fill="#34A853"
|
|
69
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
70
|
+
/>
|
|
71
|
+
<path
|
|
72
|
+
fill="#FBBC05"
|
|
73
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
74
|
+
/>
|
|
75
|
+
<path
|
|
76
|
+
fill="#EA4335"
|
|
77
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
<span className="text-sm text-gray-700 font-bold">
|
|
81
|
+
{loading ? "Connecting..." : "Continue with Google"}
|
|
82
|
+
</span>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export { default } from "./segment"
|
|
2
2
|
export { default as manifest } from "./manifest"
|
|
3
|
+
export { default as ResetPasswordPage } from "./reset-password-page"
|
|
4
|
+
export { default as OtpVerificationModal } from "./otp-verification-modal"
|
|
5
|
+
export { default as CommerceAuthOtpModal } from "./commerce-auth-otp-modal"
|
|
6
|
+
export type { OtpVerificationModalProps } from "./otp-verification-modal"
|
|
7
|
+
export { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
|