@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-login-template",
3
- "version": "0.2.4",
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/segment-primitives": "0.3.0",
25
- "@pradip1995/segment-tokens": "0.3.2"
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&apos;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
@@ -1,2 +1,3 @@
1
1
  export { default } from "./segment"
2
2
  export { default as manifest } from "./manifest"
3
+ export { default as ResetPasswordPage } from "./reset-password-page"