@pradip1995/commerce-auth 1.0.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/README.md +9 -0
- package/package.json +54 -0
- package/src/components/forgot-password/index.tsx +179 -0
- package/src/components/login/index.tsx +606 -0
- package/src/components/register/index.tsx +581 -0
- package/src/data/customer-registration.ts +365 -0
- package/src/data/customer.ts +638 -0
- package/src/data/guest.ts +357 -0
- package/src/index.ts +12 -0
- package/src/templates/login-template.tsx +44 -0
- package/src/types/account.ts +21 -0
- package/src/util-google-oauth.ts +28 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { login, initiateGoogleAuth } from "@core/data/customer"
|
|
4
|
+
import { LOGIN_VIEW } from "@core/types/account"
|
|
5
|
+
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
6
|
+
import Envelope from "@modules/common/icons/envelope"
|
|
7
|
+
import Lock from "@modules/common/icons/lock"
|
|
8
|
+
import Eye from "@modules/common/icons/eye"
|
|
9
|
+
import EyeOff from "@modules/common/icons/eye-off"
|
|
10
|
+
import Image from "next/image"
|
|
11
|
+
import { useEffect, useRef, useState } from "react"
|
|
12
|
+
import { useRouter, useParams, useSearchParams } from "next/navigation"
|
|
13
|
+
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
14
|
+
import { useServerAction } from "@core/hooks/use-server-action"
|
|
15
|
+
|
|
16
|
+
import { sendOTP, verifyOTP, listGuestOrders } from "@core/data/guest"
|
|
17
|
+
import { Button, Input, Heading, Text, clx } from "@medusajs/ui"
|
|
18
|
+
import Modal from "@modules/common/components/modal"
|
|
19
|
+
import { signout } from "@core/data/customer"
|
|
20
|
+
import DeletionPendingModal from "@modules/account/components/deletion-pending-modal"
|
|
21
|
+
import Spinner from "@modules/common/icons/spinner"
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
setCurrentView: (view: LOGIN_VIEW) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
import PhoneInput from "react-phone-input-2"
|
|
28
|
+
import "react-phone-input-2/lib/style.css"
|
|
29
|
+
import Phone from "@modules/common/icons/phone"
|
|
30
|
+
|
|
31
|
+
const Login = ({ setCurrentView }: Props) => {
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
const params = useParams()
|
|
34
|
+
const searchParams = useSearchParams()
|
|
35
|
+
const {
|
|
36
|
+
execute: submitLogin,
|
|
37
|
+
isPending,
|
|
38
|
+
result: message,
|
|
39
|
+
} = useServerAction((formData: FormData) => login(null, formData))
|
|
40
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
41
|
+
const [loginMethod, setLoginMethod] = useState<"email" | "phone">("email")
|
|
42
|
+
const [googleLoading, setGoogleLoading] = useState(false)
|
|
43
|
+
const [showGuestModal, setShowGuestModal] = useState(false)
|
|
44
|
+
const [checkingGuest, setCheckingGuest] = useState(false)
|
|
45
|
+
const [showDeletionModal, setShowDeletionModal] = useState(false)
|
|
46
|
+
const [emailForCancellation, setEmailForCancellation] = useState("")
|
|
47
|
+
const emailRef = useRef<HTMLInputElement>(null)
|
|
48
|
+
const [redirectUrl, setRedirectUrl] = useState<string | null>(null)
|
|
49
|
+
const [identifier, setIdentifier] = useState("")
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Check if there's a stored redirect URL
|
|
53
|
+
const storedRedirect = localStorage.getItem("loginRedirectUrl")
|
|
54
|
+
if (storedRedirect) {
|
|
55
|
+
setRedirectUrl(storedRedirect)
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
// Handle account deletion pending error
|
|
61
|
+
if (message === "ACCOUNT_DELETION_PENDING") {
|
|
62
|
+
setEmailForCancellation(emailRef.current?.value || "")
|
|
63
|
+
setShowDeletionModal(true)
|
|
64
|
+
}
|
|
65
|
+
}, [message, params, router])
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const view = searchParams.get("view")
|
|
69
|
+
const email = searchParams.get("email")
|
|
70
|
+
|
|
71
|
+
if (view === "guest" && email && email !== "undefined") {
|
|
72
|
+
setShowGuestModal(true)
|
|
73
|
+
}
|
|
74
|
+
}, [searchParams])
|
|
75
|
+
|
|
76
|
+
const handleTrackGuestOrder = async () => {
|
|
77
|
+
const getCookie = (name: string) => {
|
|
78
|
+
if (typeof document === "undefined") return null
|
|
79
|
+
const value = `; ${document.cookie}`
|
|
80
|
+
const parts = value.split(`; ${name}=`)
|
|
81
|
+
if (parts.length === 2) return parts.pop()?.split(";").shift()
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const token = getCookie("_medusa_guest_token")
|
|
86
|
+
|
|
87
|
+
if (token) {
|
|
88
|
+
setCheckingGuest(true)
|
|
89
|
+
try {
|
|
90
|
+
// Verify token by trying to list orders
|
|
91
|
+
await listGuestOrders(token)
|
|
92
|
+
|
|
93
|
+
// If successful, redirect
|
|
94
|
+
const countryCode = (params?.countryCode as string) || "in"
|
|
95
|
+
router.push(`/${countryCode}/guest-orders`)
|
|
96
|
+
return
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Token invalid, clear it and show modal
|
|
99
|
+
document.cookie =
|
|
100
|
+
"_medusa_guest_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
101
|
+
} finally {
|
|
102
|
+
setCheckingGuest(false)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setShowGuestModal(true)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="w-full flex flex-col" data-testid="login-page">
|
|
111
|
+
<h1 className="text-xl min-[340px]:text-2xl min-[550px]:text-3xl font-bold text-heading mb-1 min-[340px]:mb-2 text-center min-[550px]:text-left">
|
|
112
|
+
Welcome Back
|
|
113
|
+
</h1>
|
|
114
|
+
<p className="text-sm min-[340px]:text-base text-gray-500 mb-6 text-center min-[550px]:text-left">
|
|
115
|
+
Log in to your account to continue
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
{/* Google Login at Top */}
|
|
119
|
+
<div className="mb-6">
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={async (e) => {
|
|
123
|
+
e.preventDefault()
|
|
124
|
+
e.stopPropagation()
|
|
125
|
+
try {
|
|
126
|
+
setGoogleLoading(true)
|
|
127
|
+
|
|
128
|
+
// Save current country code before redirecting to Google OAuth
|
|
129
|
+
const countryCode =
|
|
130
|
+
(params?.countryCode as string) ||
|
|
131
|
+
window.location.pathname.split("/")[1] ||
|
|
132
|
+
"us"
|
|
133
|
+
localStorage.setItem("googleLoginCountryCode", countryCode)
|
|
134
|
+
|
|
135
|
+
document.cookie =
|
|
136
|
+
"_medusa_jwt=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
137
|
+
document.cookie =
|
|
138
|
+
"_medusa_cache_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
139
|
+
|
|
140
|
+
// Use server-side action instead of direct backend call
|
|
141
|
+
const result = await initiateGoogleAuth(countryCode)
|
|
142
|
+
|
|
143
|
+
if (result.error) {
|
|
144
|
+
throw new Error(result.error)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const redirectUrl = result.redirectUrl
|
|
148
|
+
|
|
149
|
+
if (redirectUrl) {
|
|
150
|
+
window.location.href = redirectUrl
|
|
151
|
+
} else {
|
|
152
|
+
setGoogleLoading(false)
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
setGoogleLoading(false)
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
disabled={googleLoading}
|
|
159
|
+
className="w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-white hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
160
|
+
style={{ borderRadius: "30px" }}
|
|
161
|
+
>
|
|
162
|
+
<Image src="/Google.svg" alt="Google" width={20} height={20} />
|
|
163
|
+
<span className="text-sm text-gray-700 font-bold">
|
|
164
|
+
{googleLoading ? "Connecting..." : "Continue with Google"}
|
|
165
|
+
</span>
|
|
166
|
+
</button>
|
|
167
|
+
|
|
168
|
+
{/* Quick Link at Top */}
|
|
169
|
+
<div className="mt-4 text-center">
|
|
170
|
+
<span className="text-gray-700 text-xs min-[340px]:text-sm">
|
|
171
|
+
New to Chocomelon?{" "}
|
|
172
|
+
<button
|
|
173
|
+
onClick={() => setCurrentView(LOGIN_VIEW.REGISTER)}
|
|
174
|
+
className="text-brand-accent font-bold hover:underline"
|
|
175
|
+
>
|
|
176
|
+
Register
|
|
177
|
+
</button>
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="relative mb-6">
|
|
183
|
+
<div className="absolute inset-0 flex items-center">
|
|
184
|
+
<div className="w-full border-t border-gray-100"></div>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="relative flex justify-center text-xs uppercase tracking-widest">
|
|
187
|
+
<span className="px-3 bg-page-bg text-gray-400 font-bold">Or login with</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<form
|
|
191
|
+
className="w-full"
|
|
192
|
+
onSubmit={(event) => {
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
submitLogin(new FormData(event.currentTarget))
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<input
|
|
198
|
+
type="hidden"
|
|
199
|
+
name="country_code"
|
|
200
|
+
value={(params?.countryCode as string) || "in"}
|
|
201
|
+
/>
|
|
202
|
+
{redirectUrl && <input type="hidden" name="redirect_url" value={redirectUrl} />}
|
|
203
|
+
|
|
204
|
+
{/* Smart Identifier Input */}
|
|
205
|
+
<div className="mb-4">
|
|
206
|
+
{loginMethod === "email" ? (
|
|
207
|
+
<div className="relative group">
|
|
208
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none transition-colors group-focus-within:text-brand-accent">
|
|
209
|
+
<Envelope size="20" />
|
|
210
|
+
</div>
|
|
211
|
+
<input
|
|
212
|
+
type="text"
|
|
213
|
+
name="email_or_phone"
|
|
214
|
+
ref={emailRef}
|
|
215
|
+
value={identifier}
|
|
216
|
+
placeholder="Email or Mobile Number"
|
|
217
|
+
required
|
|
218
|
+
autoFocus={loginMethod === "email"}
|
|
219
|
+
onChange={(e) => {
|
|
220
|
+
const value = e.target.value
|
|
221
|
+
setIdentifier(value)
|
|
222
|
+
const hasLetter = /[a-zA-Z]/.test(value)
|
|
223
|
+
const isNumeric = /^\+?[\d\s\-()]*$/.test(value) && value.length > 0
|
|
224
|
+
|
|
225
|
+
if (hasLetter) setLoginMethod("email")
|
|
226
|
+
else if (isNumeric) setLoginMethod("phone")
|
|
227
|
+
}}
|
|
228
|
+
className="w-full pl-12 pr-4 py-3 min-[550px]:py-3.5 text-sm min-[340px]:text-base border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-brand-accent/20 focus:border-brand-accent transition-all shadow-sm"
|
|
229
|
+
style={{ borderRadius: "30px" }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
) : (
|
|
233
|
+
<div className="phone-input-login relative group">
|
|
234
|
+
<PhoneInput
|
|
235
|
+
country={"in"}
|
|
236
|
+
value={identifier}
|
|
237
|
+
onChange={(value) => {
|
|
238
|
+
setIdentifier(value)
|
|
239
|
+
const hasLetter = /[a-zA-Z]/.test(value)
|
|
240
|
+
if (hasLetter || value === "") setLoginMethod("email")
|
|
241
|
+
}}
|
|
242
|
+
inputProps={{
|
|
243
|
+
name: "email_or_phone",
|
|
244
|
+
required: true,
|
|
245
|
+
autoFocus: true,
|
|
246
|
+
}}
|
|
247
|
+
placeholder="Mobile Number"
|
|
248
|
+
containerStyle={{ width: "100%" }}
|
|
249
|
+
inputStyle={{
|
|
250
|
+
width: "100%",
|
|
251
|
+
height: "52px",
|
|
252
|
+
fontSize: "16px",
|
|
253
|
+
paddingLeft: "55px",
|
|
254
|
+
borderRadius: "30px",
|
|
255
|
+
border: "1px solid #e5e7eb",
|
|
256
|
+
backgroundColor: "var(--color-surface)",
|
|
257
|
+
}}
|
|
258
|
+
buttonStyle={{
|
|
259
|
+
backgroundColor: "transparent",
|
|
260
|
+
border: "none",
|
|
261
|
+
borderRadius: "30px 0 0 30px",
|
|
262
|
+
paddingLeft: "12px",
|
|
263
|
+
}}
|
|
264
|
+
dropdownStyle={{
|
|
265
|
+
width: "280px",
|
|
266
|
+
borderRadius: "15px",
|
|
267
|
+
zIndex: 50,
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
<style jsx global>{`
|
|
271
|
+
.phone-input-login .react-tel-input .form-control:focus {
|
|
272
|
+
outline: none !important;
|
|
273
|
+
border-color: var(--color-brand-accent) !important;
|
|
274
|
+
box-shadow: 0 0 0 2px #f3e8ff !important;
|
|
275
|
+
}
|
|
276
|
+
`}</style>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Password Input */}
|
|
282
|
+
<div className="mb-4">
|
|
283
|
+
<div className="relative">
|
|
284
|
+
<div className="absolute inset-y-0 left-0 pl-3 min-[340px]:pl-4 flex items-center pointer-events-none">
|
|
285
|
+
<Lock size="20" color="#9CA3AF" />
|
|
286
|
+
</div>
|
|
287
|
+
<input
|
|
288
|
+
type={showPassword ? "text" : "password"}
|
|
289
|
+
name="password"
|
|
290
|
+
placeholder="Password"
|
|
291
|
+
autoComplete="current-password"
|
|
292
|
+
required
|
|
293
|
+
className="w-full pl-10 min-[340px]:pl-12 pr-10 min-[340px]:pr-12 py-2 min-[340px]:py-2.5 min-[550px]:py-3 text-sm min-[340px]:text-base border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent focus:border-transparent"
|
|
294
|
+
style={{ borderRadius: "30px" }}
|
|
295
|
+
data-testid="password-input"
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
300
|
+
className="absolute inset-y-0 right-0 pr-3 min-[340px]:pr-4 flex items-center text-gray-500 hover:text-gray-700"
|
|
301
|
+
>
|
|
302
|
+
{showPassword ? <EyeOff size="20" /> : <Eye size="20" />}
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Forgot Password Link */}
|
|
308
|
+
<div className="flex justify-end mb-4 min-[340px]:mb-5 min-[550px]:mb-6">
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={() => setCurrentView(LOGIN_VIEW.FORGOT_PASSWORD)}
|
|
312
|
+
className="text-xs min-[340px]:text-sm text-gray-700 hover:text-brand-accent"
|
|
313
|
+
>
|
|
314
|
+
Forgot password?
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<ErrorMessage
|
|
319
|
+
error={message === "ACCOUNT_DELETION_PENDING" ? "" : message}
|
|
320
|
+
data-testid="login-error-message"
|
|
321
|
+
/>
|
|
322
|
+
|
|
323
|
+
{/* Terms and Conditions */}
|
|
324
|
+
<div className="mb-4 min-[340px]:mb-5 min-[550px]:mb-6">
|
|
325
|
+
<span className="text-center text-gray-700 text-xs min-[340px]:text-sm">
|
|
326
|
+
By logging in, you agree to Chocomelon's{" "}
|
|
327
|
+
<LocalizedClientLink
|
|
328
|
+
href="/privacy-policy"
|
|
329
|
+
className="text-brand-accent font-semibold hover:underline"
|
|
330
|
+
>
|
|
331
|
+
Privacy Policy
|
|
332
|
+
</LocalizedClientLink>{" "}
|
|
333
|
+
and{" "}
|
|
334
|
+
<LocalizedClientLink
|
|
335
|
+
href="/terms-of-use"
|
|
336
|
+
className="text-brand-accent font-semibold hover:underline"
|
|
337
|
+
>
|
|
338
|
+
Terms of Use
|
|
339
|
+
</LocalizedClientLink>
|
|
340
|
+
.
|
|
341
|
+
</span>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{/* Login Button */}
|
|
345
|
+
<button
|
|
346
|
+
type="submit"
|
|
347
|
+
disabled={isPending}
|
|
348
|
+
className="w-full py-2.5 min-[340px]:py-3 px-3 min-[340px]:px-4 bg-brand-accent text-sm min-[340px]:text-base text-inverse font-bold hover:bg-brand-accent-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-4 min-[340px]:mb-5 min-[550px]:mb-6"
|
|
349
|
+
style={{ borderRadius: "30px" }}
|
|
350
|
+
onClick={() => {
|
|
351
|
+
if (typeof window !== "undefined") {
|
|
352
|
+
// Wait slightly to make sure the form value is captured,
|
|
353
|
+
// though React state is already set.
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
localStorage.removeItem("loginRedirectUrl")
|
|
356
|
+
}, 100)
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
data-testid="sign-in-button"
|
|
360
|
+
>
|
|
361
|
+
{isPending ? "Logging in..." : "Login"}
|
|
362
|
+
</button>
|
|
363
|
+
</form>
|
|
364
|
+
|
|
365
|
+
{/* Guest Order Tracking Button */}
|
|
366
|
+
|
|
367
|
+
{/* Guest Order Tracking Button */}
|
|
368
|
+
<div className="mt-6 pt-6 border-t border-gray-200">
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={handleTrackGuestOrder}
|
|
372
|
+
disabled={checkingGuest}
|
|
373
|
+
className="w-full py-2.5 px-4 rounded-full border border-brand-accent text-brand-accent text-sm font-bold hover:bg-brand-accent-muted transition-colors flex items-center justify-center gap-2"
|
|
374
|
+
>
|
|
375
|
+
{checkingGuest ? <Spinner className="animate-spin" /> : <span>📦</span>}
|
|
376
|
+
{checkingGuest ? "Checking..." : "Track Guest Order"}
|
|
377
|
+
</button>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<DeletionPendingModal
|
|
381
|
+
isOpen={showDeletionModal}
|
|
382
|
+
close={() => setShowDeletionModal(false)}
|
|
383
|
+
email={emailForCancellation}
|
|
384
|
+
countryCode={params.countryCode as string}
|
|
385
|
+
/>
|
|
386
|
+
|
|
387
|
+
<GuestOrderModal
|
|
388
|
+
isOpen={showGuestModal}
|
|
389
|
+
close={() => setShowGuestModal(false)}
|
|
390
|
+
router={router}
|
|
391
|
+
params={params}
|
|
392
|
+
initialEmail={searchParams.get("email") || ""}
|
|
393
|
+
autoStart={searchParams.get("view") === "guest" && !!searchParams.get("email")}
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const GuestOrderModal = ({
|
|
400
|
+
isOpen,
|
|
401
|
+
close,
|
|
402
|
+
router,
|
|
403
|
+
params,
|
|
404
|
+
initialEmail = "",
|
|
405
|
+
autoStart = false,
|
|
406
|
+
}: {
|
|
407
|
+
isOpen: boolean
|
|
408
|
+
close: () => void
|
|
409
|
+
router: any
|
|
410
|
+
params: any
|
|
411
|
+
initialEmail?: string
|
|
412
|
+
autoStart?: boolean
|
|
413
|
+
}) => {
|
|
414
|
+
const [email, setEmail] = useState(initialEmail)
|
|
415
|
+
const [otp, setOtp] = useState("")
|
|
416
|
+
const [step, setStep] = useState<"email" | "otp">("email")
|
|
417
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
418
|
+
const [error, setError] = useState<string | null>(null)
|
|
419
|
+
const [verificationToken, setVerificationToken] = useState<string | null>(null)
|
|
420
|
+
const [success, setSuccess] = useState(false)
|
|
421
|
+
const autoStartedRef = useRef(false)
|
|
422
|
+
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (initialEmail) setEmail(initialEmail)
|
|
425
|
+
}, [initialEmail])
|
|
426
|
+
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
// If autoStart is true and we have an email, trigger OTP send immediately
|
|
429
|
+
if (isOpen && autoStart && initialEmail && !autoStartedRef.current) {
|
|
430
|
+
autoStartedRef.current = true
|
|
431
|
+
setIsLoading(true)
|
|
432
|
+
handleSendOtp(initialEmail)
|
|
433
|
+
}
|
|
434
|
+
}, [isOpen, autoStart, initialEmail])
|
|
435
|
+
|
|
436
|
+
const handleSendOtp = async (emailToSend = email) => {
|
|
437
|
+
if (!emailToSend) return
|
|
438
|
+
setIsLoading(true)
|
|
439
|
+
setError(null)
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const res = (await sendOTP(emailToSend, "email_verification")) as any
|
|
443
|
+
if (res.token || res.success !== false) {
|
|
444
|
+
setVerificationToken(res.token || "temp-token")
|
|
445
|
+
setStep("otp")
|
|
446
|
+
} else {
|
|
447
|
+
setError((res as any).error || "Failed to send OTP")
|
|
448
|
+
setStep("email")
|
|
449
|
+
}
|
|
450
|
+
} catch (e: any) {
|
|
451
|
+
setError(e.message)
|
|
452
|
+
setStep("email")
|
|
453
|
+
} finally {
|
|
454
|
+
setIsLoading(false)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const handleVerifyOtp = async () => {
|
|
459
|
+
if (!otp) return
|
|
460
|
+
setIsLoading(true)
|
|
461
|
+
setError(null)
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
// Pass email as customerId since it's a guest flow
|
|
465
|
+
const res = (await verifyOTP(email, verificationToken || "", otp)) as any
|
|
466
|
+
if (res.success !== false) {
|
|
467
|
+
// Assuming success if no explicit error field
|
|
468
|
+
setSuccess(true)
|
|
469
|
+
|
|
470
|
+
// Store token in cookie
|
|
471
|
+
// Note: For a real app, use a server action to set httpOnly cookie.
|
|
472
|
+
// Here we simulate for client-side usage or basic auth header.
|
|
473
|
+
const token = res.token || "guest_access_token"
|
|
474
|
+
document.cookie = `_medusa_guest_token=${token}; path=/; max-age=3600; SameSite=Lax`
|
|
475
|
+
|
|
476
|
+
// Redirect to guest order view
|
|
477
|
+
const countryCode = (params?.countryCode as string) || "in"
|
|
478
|
+
router.push(`/${countryCode}/guest-orders`)
|
|
479
|
+
} else {
|
|
480
|
+
setError(res.error || "Invalid OTP")
|
|
481
|
+
}
|
|
482
|
+
} catch (e: any) {
|
|
483
|
+
setError(e.message)
|
|
484
|
+
} finally {
|
|
485
|
+
setIsLoading(false)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const reset = () => {
|
|
490
|
+
setEmail("")
|
|
491
|
+
setOtp("")
|
|
492
|
+
setStep("email")
|
|
493
|
+
setError(null)
|
|
494
|
+
setSuccess(false)
|
|
495
|
+
close()
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<Modal isOpen={isOpen} close={reset} size="small">
|
|
500
|
+
<Modal.Body>
|
|
501
|
+
<div className="flex flex-col gap-6 py-4 px-2">
|
|
502
|
+
{/* Header */}
|
|
503
|
+
<div className="text-center">
|
|
504
|
+
<h2 className="text-2xl font-bold text-heading mb-2">
|
|
505
|
+
{step === "email" ? "Track Your Order" : "Verify OTP"}
|
|
506
|
+
</h2>
|
|
507
|
+
<p className="text-sm text-gray-500 leading-relaxed max-w-[300px] mx-auto">
|
|
508
|
+
{step === "email"
|
|
509
|
+
? "Enter your email address to track your guest orders. We will send you a One-Time Password (OTP)."
|
|
510
|
+
: `Enter the 6-digit code sent to ${email}`}
|
|
511
|
+
</p>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
{success ? (
|
|
515
|
+
<div className="flex flex-col items-center gap-4 py-6">
|
|
516
|
+
<div className="text-brand-accent animate-pulse font-bold">
|
|
517
|
+
Verification Successful!
|
|
518
|
+
</div>
|
|
519
|
+
<Spinner />
|
|
520
|
+
</div>
|
|
521
|
+
) : step === "email" ? (
|
|
522
|
+
<div className="space-y-4">
|
|
523
|
+
<div className="relative">
|
|
524
|
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-gray-400">
|
|
525
|
+
<Envelope size="20" />
|
|
526
|
+
</div>
|
|
527
|
+
<input
|
|
528
|
+
placeholder="Email"
|
|
529
|
+
value={email}
|
|
530
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
531
|
+
autoComplete="email"
|
|
532
|
+
type="email"
|
|
533
|
+
required
|
|
534
|
+
className="w-full pl-12 pr-4 py-3 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-accent transition-all text-sm"
|
|
535
|
+
style={{ borderRadius: "30px" }}
|
|
536
|
+
/>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
540
|
+
|
|
541
|
+
<button
|
|
542
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
543
|
+
style={{ borderRadius: "30px" }}
|
|
544
|
+
onClick={() => handleSendOtp()}
|
|
545
|
+
disabled={isLoading || !email}
|
|
546
|
+
>
|
|
547
|
+
{isLoading ? <Spinner size={16} /> : "Send OTP"}
|
|
548
|
+
</button>
|
|
549
|
+
</div>
|
|
550
|
+
) : (
|
|
551
|
+
<div className="space-y-4">
|
|
552
|
+
<div className="relative flex justify-center gap-x-2.5 mt-6">
|
|
553
|
+
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
554
|
+
<div
|
|
555
|
+
key={index}
|
|
556
|
+
className={`w-10 h-12 flex items-center justify-center border-2 rounded-lg text-xl font-bold transition-all duration-200 ${
|
|
557
|
+
otp.length === index
|
|
558
|
+
? "border-brand-accent ring-2 ring-brand-accent/10 bg-page-bg"
|
|
559
|
+
: otp.length > index
|
|
560
|
+
? "border-gray-400 bg-page-bg shadow-sm"
|
|
561
|
+
: "border-gray-200 bg-gray-50/50"
|
|
562
|
+
}`}
|
|
563
|
+
>
|
|
564
|
+
{otp[index] || ""}
|
|
565
|
+
</div>
|
|
566
|
+
))}
|
|
567
|
+
<input
|
|
568
|
+
type="text"
|
|
569
|
+
maxLength={6}
|
|
570
|
+
className="absolute inset-0 opacity-0 cursor-pointer"
|
|
571
|
+
value={otp}
|
|
572
|
+
onChange={(e) =>
|
|
573
|
+
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
|
|
574
|
+
}
|
|
575
|
+
autoFocus
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
{error && <p className="text-red-500 text-xs text-center">{error}</p>}
|
|
580
|
+
|
|
581
|
+
<div className="space-y-3">
|
|
582
|
+
<button
|
|
583
|
+
className="w-full py-3 bg-brand-accent text-inverse font-bold hover:bg-brand-accent-hover transition-colors flex justify-center items-center gap-2"
|
|
584
|
+
style={{ borderRadius: "30px" }}
|
|
585
|
+
onClick={handleVerifyOtp}
|
|
586
|
+
disabled={isLoading || otp.length < 6}
|
|
587
|
+
>
|
|
588
|
+
{isLoading ? <Spinner size={16} /> : "Verify & View Orders"}
|
|
589
|
+
</button>
|
|
590
|
+
|
|
591
|
+
<button
|
|
592
|
+
onClick={() => setStep("email")}
|
|
593
|
+
className="text-xs text-gray-500 hover:text-brand-accent w-full text-center underline font-medium"
|
|
594
|
+
>
|
|
595
|
+
Change Email
|
|
596
|
+
</button>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
</Modal.Body>
|
|
602
|
+
</Modal>
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export default Login
|