@pradip1995/segment-login-template 0.2.4 → 0.3.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 +8 -4
- package/src/account-overview.tsx +69 -0
- package/src/auth-server.ts +152 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +25 -0
- package/src/index.ts +1 -0
- package/src/login-form.tsx +355 -0
- package/src/otp-input.tsx +46 -0
- package/src/register-form.tsx +298 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +19 -196
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-login-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
],
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/index.ts",
|
|
14
|
-
"./manifest": "./src/manifest.ts"
|
|
14
|
+
"./manifest": "./src/manifest.ts",
|
|
15
|
+
"./reset-password": "./src/reset-password-page.tsx"
|
|
15
16
|
},
|
|
16
17
|
"peerDependencies": {
|
|
18
|
+
"@pradip1995/commerce-auth": "^4.0.0",
|
|
17
19
|
"@pradip1995/commerce-core": "^4.0.0",
|
|
18
20
|
"@pradip1995/plugin-sdk": "^0.2.0",
|
|
19
21
|
"react": ">=19",
|
|
@@ -21,8 +23,10 @@
|
|
|
21
23
|
"next": ">=15"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
|
-
"@pradip1995/
|
|
25
|
-
"@pradip1995/
|
|
26
|
+
"@pradip1995/commerce-auth": "^4.0.0",
|
|
27
|
+
"@pradip1995/commerce-core": "^4.0.0",
|
|
28
|
+
"@pradip1995/segment-primitives": "^0.3.0",
|
|
29
|
+
"@pradip1995/segment-tokens": "^0.3.2"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@pradip1995/plugin-sdk": "^0.2.0",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { signout } from "@pradip1995/commerce-core/client/actions/customer"
|
|
4
|
+
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
5
|
+
import { formatPrice } from "@pradip1995/segment-primitives/format-price"
|
|
6
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
7
|
+
|
|
8
|
+
export default function AccountOverview({
|
|
9
|
+
customer,
|
|
10
|
+
orders,
|
|
11
|
+
countryCode,
|
|
12
|
+
}: {
|
|
13
|
+
customer: HttpTypes.StoreCustomer
|
|
14
|
+
orders: HttpTypes.StoreOrder[]
|
|
15
|
+
countryCode: string
|
|
16
|
+
}) {
|
|
17
|
+
const name = [customer.first_name, customer.last_name].filter(Boolean).join(" ") || customer.email
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-8">
|
|
21
|
+
<div className="card-surface rounded-lg p-6">
|
|
22
|
+
<p className="text-xs uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-2">My account</p>
|
|
23
|
+
<h1 className="section-heading text-xl mb-1">{name}</h1>
|
|
24
|
+
<p className="text-sm text-muted">{customer.email}</p>
|
|
25
|
+
{customer.phone && <p className="text-sm text-muted mt-1">{customer.phone}</p>}
|
|
26
|
+
<form action={signout.bind(null, countryCode)} className="mt-6">
|
|
27
|
+
<button type="submit" className="btn-outline text-xs">
|
|
28
|
+
Sign out
|
|
29
|
+
</button>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div>
|
|
34
|
+
<h2 className="text-sm font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading mb-4">
|
|
35
|
+
Recent orders
|
|
36
|
+
</h2>
|
|
37
|
+
{orders.length === 0 ? (
|
|
38
|
+
<div className="card-surface rounded-lg p-8 text-center">
|
|
39
|
+
<p className="text-muted text-sm mb-4">No orders yet.</p>
|
|
40
|
+
<LocalizedLink href="/store" className="btn-primary inline-block">
|
|
41
|
+
Start shopping
|
|
42
|
+
</LocalizedLink>
|
|
43
|
+
</div>
|
|
44
|
+
) : (
|
|
45
|
+
<ul className="space-y-3">
|
|
46
|
+
{orders.map((order) => (
|
|
47
|
+
<li key={order.id}>
|
|
48
|
+
<LocalizedLink
|
|
49
|
+
href={`/orders/${order.id}`}
|
|
50
|
+
className="card-surface rounded-lg p-4 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
|
|
51
|
+
>
|
|
52
|
+
<div>
|
|
53
|
+
<p className="font-medium text-heading text-sm">Order #{order.display_id}</p>
|
|
54
|
+
<p className="text-xs text-muted mt-1 capitalize">
|
|
55
|
+
{order.status?.replace(/_/g, " ")}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
<p className="text-sm font-semibold text-brand-accent shrink-0">
|
|
59
|
+
{formatPrice(order.total, order.currency_code)}
|
|
60
|
+
</p>
|
|
61
|
+
</LocalizedLink>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use server"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
checkEmailRegistered,
|
|
5
|
+
completePasswordReset,
|
|
6
|
+
registerCustomer,
|
|
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 { revalidateTag } from "next/cache"
|
|
15
|
+
import { redirect } from "next/navigation"
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
registerCustomer,
|
|
19
|
+
sendCustomerOTP,
|
|
20
|
+
verifyCustomerOTP,
|
|
21
|
+
requestPasswordReset,
|
|
22
|
+
checkEmailRegistered,
|
|
23
|
+
completePasswordReset,
|
|
24
|
+
sendOTP,
|
|
25
|
+
verifyOTP,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getHeaders() {
|
|
29
|
+
return {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || "",
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getBaseUrl() {
|
|
36
|
+
return process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function sendAuthOtp(input: {
|
|
40
|
+
email?: string
|
|
41
|
+
phone?: string
|
|
42
|
+
type: "email_auth" | "phone_auth"
|
|
43
|
+
}) {
|
|
44
|
+
const baseUrl = getBaseUrl()
|
|
45
|
+
const body =
|
|
46
|
+
input.type === "email_auth"
|
|
47
|
+
? { email: input.email?.toLowerCase().trim(), type: input.type }
|
|
48
|
+
: { phone: input.phone?.replace(/[^\d+]/g, ""), type: input.type }
|
|
49
|
+
|
|
50
|
+
const response = await fetch(`${baseUrl}/store/customers/otp/send`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: getHeaders(),
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const err = await response.json().catch(() => ({}))
|
|
58
|
+
return {
|
|
59
|
+
success: false as const,
|
|
60
|
+
error: (err as { message?: string }).message || "Failed to send verification code",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = (await response.json()) as {
|
|
65
|
+
token?: string
|
|
66
|
+
expires_at?: string
|
|
67
|
+
is_new_user?: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: true as const,
|
|
72
|
+
token: data.token,
|
|
73
|
+
isNewUser: data.is_new_user ?? false,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function verifyAuthOtpAndLogin(input: {
|
|
78
|
+
token: string
|
|
79
|
+
code: string
|
|
80
|
+
countryCode: string
|
|
81
|
+
first_name?: string
|
|
82
|
+
last_name?: string
|
|
83
|
+
}) {
|
|
84
|
+
const baseUrl = getBaseUrl()
|
|
85
|
+
|
|
86
|
+
const response = await fetch(`${baseUrl}/store/customers/otp/verify`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: getHeaders(),
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
token: input.token,
|
|
91
|
+
code: input.code,
|
|
92
|
+
first_name: input.first_name,
|
|
93
|
+
last_name: input.last_name,
|
|
94
|
+
}),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const err = await response.json().catch(() => ({}))
|
|
99
|
+
return {
|
|
100
|
+
success: false as const,
|
|
101
|
+
error: (err as { message?: string }).message || "Invalid verification code",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = (await response.json()) as {
|
|
106
|
+
token?: string | null
|
|
107
|
+
verified?: boolean
|
|
108
|
+
is_new_user?: boolean
|
|
109
|
+
customer?: { email?: string }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const jwt = data.token
|
|
113
|
+
if (!jwt) {
|
|
114
|
+
return {
|
|
115
|
+
success: false as const,
|
|
116
|
+
error: "Verification succeeded but login token was not returned. Try signing in with your password.",
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await setAuthToken(jwt)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await transferCart(jwt)
|
|
124
|
+
} catch {
|
|
125
|
+
// non-fatal
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const customerCacheTag = await getCacheTag("customers")
|
|
129
|
+
revalidateTag(customerCacheTag)
|
|
130
|
+
|
|
131
|
+
const countryCode = input.countryCode || "in"
|
|
132
|
+
redirect(`/${countryCode}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function completePasswordResetAction(input: {
|
|
136
|
+
email: string
|
|
137
|
+
password: string
|
|
138
|
+
token: string
|
|
139
|
+
countryCode: string
|
|
140
|
+
}) {
|
|
141
|
+
const result = await completePasswordReset({
|
|
142
|
+
email: input.email,
|
|
143
|
+
password: input.password,
|
|
144
|
+
token: input.token,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!result.success) {
|
|
148
|
+
return result
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
redirect(`/${input.countryCode || "in"}/account`)
|
|
152
|
+
}
|
|
@@ -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,25 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { initiateGoogleAuth } from "@pradip1995/commerce-core/client/actions/customer"
|
|
4
|
+
import { GoogleLoginButton } from "@pradip1995/commerce-auth/components/google-login"
|
|
5
|
+
|
|
6
|
+
const BUTTON_CLASS =
|
|
7
|
+
"btn-outline w-full flex items-center justify-center gap-2.5 py-3 px-4"
|
|
8
|
+
|
|
9
|
+
export default function GoogleAuthSection({ countryCode }: { countryCode: string }) {
|
|
10
|
+
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
|
|
11
|
+
|
|
12
|
+
if (!clientId) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="mb-6">
|
|
16
|
+
<GoogleLoginButton
|
|
17
|
+
countryCode={countryCode}
|
|
18
|
+
defaultCountryCode="in"
|
|
19
|
+
initiateAuth={initiateGoogleAuth}
|
|
20
|
+
className={BUTTON_CLASS}
|
|
21
|
+
googleIconSrc="/Google.svg"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
package/src/index.ts
CHANGED