@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.
@@ -0,0 +1,357 @@
1
+ "use server"
2
+
3
+ import { cookies, headers } from "next/headers"
4
+ import { getGuestAuthHeaders } from "./cookies"
5
+ import { sdk } from "@core/config"
6
+
7
+ const BACKEND_URL =
8
+ process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || "http://localhost:9000"
9
+ const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
10
+
11
+ /**
12
+ * Compatibility wrapper for sendOTP
13
+ */
14
+ export async function sendOTP(email: string, type: string = "email_verification") {
15
+ const res = await requestGuestOTP(email)
16
+ // The previous project expected a token or success.
17
+ // We return a dummy token if successful because the UI checks for it.
18
+ if (res.success) {
19
+ return { success: true, token: "otp_sent_successfully" }
20
+ }
21
+ return res
22
+ }
23
+
24
+ /**
25
+ * Compatibility wrapper for verifyOTP
26
+ */
27
+ export async function verifyOTP(email: string, token: string, otp: string) {
28
+ // The previous implementation used 3 args, but guest.ts uses 2.
29
+ // We ignore the mid 'token' as per user's working code.
30
+ return await verifyGuestOTP(email, otp)
31
+ }
32
+
33
+ import { retrieveOrder } from "./orders"
34
+
35
+ /**
36
+ * Compatibility wrapper for listGuestOrders
37
+ */
38
+ export async function listGuestOrders(token: string) {
39
+ const res = await getGuestOrders(token)
40
+ if (!res.success) {
41
+ throw new Error(res.error || "Failed to fetch orders")
42
+ }
43
+
44
+ // Fetch full details for each order to ensure latest fulfillment_status is available
45
+ // because the custom list endpoint might not return all fields correctly.
46
+ const fullOrders = await Promise.all(
47
+ (res.orders || []).map(async (order: any) => {
48
+ try {
49
+ return await retrieveOrder(order.id)
50
+ } catch (e) {
51
+ return order
52
+ }
53
+ })
54
+ )
55
+
56
+ return { orders: fullOrders, count: fullOrders.length }
57
+ }
58
+
59
+ export async function requestGuestOTP(email: string) {
60
+ const headers: Record<string, string> = {
61
+ "Content-Type": "application/json",
62
+ }
63
+ if (PUBLISHABLE_KEY) headers["x-publishable-api-key"] = PUBLISHABLE_KEY
64
+
65
+ try {
66
+ const res = await fetch(`${BACKEND_URL}/store/otp/request`, {
67
+ method: "POST",
68
+ headers,
69
+ body: JSON.stringify({ email }),
70
+ cache: "no-store",
71
+ })
72
+
73
+ if (!res.ok) {
74
+ const errorData = await res.json().catch(() => ({}))
75
+ throw new Error(errorData.message || "Failed to send OTP")
76
+ }
77
+ return { success: true }
78
+ } catch (error: any) {
79
+ return { success: false, error: error.message }
80
+ }
81
+ }
82
+
83
+ export async function verifyGuestOTP(email: string, otp: string) {
84
+ const headers: Record<string, string> = {
85
+ "Content-Type": "application/json",
86
+ }
87
+ if (PUBLISHABLE_KEY) headers["x-publishable-api-key"] = PUBLISHABLE_KEY
88
+
89
+ try {
90
+ const res = await fetch(`${BACKEND_URL}/store/otp/verify`, {
91
+ method: "POST",
92
+ headers,
93
+ body: JSON.stringify({ email, otp }),
94
+ cache: "no-store",
95
+ })
96
+
97
+ if (!res.ok) {
98
+ const errorData = await res.json().catch(() => ({}))
99
+ throw new Error(errorData.message || "Failed to verify OTP")
100
+ }
101
+
102
+ const data = await res.json()
103
+
104
+ if (data.token) {
105
+ const cookieStore = await cookies()
106
+ cookieStore.set("_medusa_guest_jwt", data.token, {
107
+ httpOnly: true,
108
+ secure: process.env.NODE_ENV === "production",
109
+ maxAge: 60 * 60 * 24, // 1 day
110
+ path: "/",
111
+ })
112
+ } else {
113
+ throw new Error("No token returned")
114
+ }
115
+
116
+ return { success: true, token: data.token }
117
+ } catch (error: any) {
118
+ return { success: false, error: error.message }
119
+ }
120
+ }
121
+
122
+ export async function logoutGuest() {
123
+ const cookieStore = await cookies()
124
+ cookieStore.set("_medusa_guest_jwt", "", {
125
+ maxAge: -1,
126
+ path: "/",
127
+ })
128
+ return { success: true }
129
+ }
130
+
131
+ export async function getGuestOrders(token?: string) {
132
+ const cookieStore = await cookies()
133
+ const headersStore = await headers()
134
+ const authHeader = headersStore.get("authorization")
135
+ const headerToken = authHeader?.startsWith("Bearer ")
136
+ ? authHeader.split(" ")[1]
137
+ : undefined
138
+
139
+ const authToken = token || headerToken || cookieStore.get("_medusa_guest_jwt")?.value
140
+ if (!authToken) return { success: false, error: "Unauthorized" }
141
+
142
+ const requestHeaders: Record<string, string> = {
143
+ Authorization: `Bearer ${authToken}`,
144
+ }
145
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
146
+
147
+ try {
148
+ const response = await sdk.client.fetch<any>(`/store/guest-orders`, {
149
+ method: "GET",
150
+ headers: requestHeaders,
151
+ query: {
152
+ fields:
153
+ "id,display_id,status,fulfillment_status,payment_status,created_at,currency_code,total,subtotal,tax_total,shipping_total,discount_total,metadata,email,*items,*items.variant,*items.variant.product",
154
+ },
155
+ cache: "no-store",
156
+ })
157
+
158
+ return { success: true, orders: response.orders || [] }
159
+ } catch (error: any) {
160
+ return { success: false, error: error.message }
161
+ }
162
+ }
163
+
164
+ export async function getGuestOrder(id: string, token?: string) {
165
+ const cookieStore = await cookies()
166
+ const headersStore = await headers()
167
+ const authHeader = headersStore.get("authorization")
168
+ const headerToken = authHeader?.startsWith("Bearer ")
169
+ ? authHeader.split(" ")[1]
170
+ : undefined
171
+
172
+ const authToken = token || headerToken || cookieStore.get("_medusa_guest_jwt")?.value
173
+ if (!authToken) return { success: false, error: "Unauthorized" }
174
+
175
+ const requestHeaders: Record<string, string> = {
176
+ Authorization: `Bearer ${authToken}`,
177
+ }
178
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
179
+
180
+ try {
181
+ const response = await sdk.client.fetch<any>(`/store/orders/${id}`, {
182
+ method: "GET",
183
+ headers: requestHeaders,
184
+ query: {
185
+ fields:
186
+ "*payment_collections.payments,*items,*items.metadata,*items.variant,*items.variant.images,*items.variant.product,*items.variant.product.thumbnail,*items.variant.product.images,*items.product,*items.product.thumbnail,*items.product.images,*fulfillments,*fulfillments.items,*fulfillments.location_id,*returns,*returns.items,*cart,*shipping_address,*billing_address,*region,*shipping_methods",
187
+ },
188
+ cache: "no-store",
189
+ })
190
+
191
+ if (response.order) {
192
+ return { success: true, order: response.order }
193
+ } else {
194
+ return { success: false, error: "Order not found" }
195
+ }
196
+ } catch (error: any) {
197
+ return { success: false, error: error.message }
198
+ }
199
+ }
200
+
201
+ export async function cancelGuestOrder(orderId: string, token?: string) {
202
+ const authHeaders = await getGuestAuthHeaders()
203
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
204
+
205
+ if (!authToken) return { success: false, error: "Unauthorized" }
206
+
207
+ const requestHeaders: Record<string, string> = {
208
+ Authorization: `Bearer ${authToken}`,
209
+ }
210
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
211
+
212
+ try {
213
+ const response = await fetch(
214
+ `${BACKEND_URL}/store/guest-orders/${orderId}/cancel`,
215
+ {
216
+ method: "POST",
217
+ headers: {
218
+ "Content-Type": "application/json",
219
+ ...requestHeaders,
220
+ },
221
+ cache: "no-store",
222
+ }
223
+ )
224
+
225
+ if (!response.ok) {
226
+ const errorData = await response.json().catch(() => ({}))
227
+ throw new Error(errorData.message || "Failed to cancel order")
228
+ }
229
+
230
+ return { success: true }
231
+ } catch (error: any) {
232
+ return { success: false, error: error.message }
233
+ }
234
+ }
235
+
236
+ export async function getGuestOrderInvoice(orderId: string, token?: string) {
237
+ const authHeaders = await getGuestAuthHeaders()
238
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
239
+
240
+ if (!authToken) return { success: false, error: "Unauthorized" }
241
+
242
+ const requestHeaders: Record<string, string> = {
243
+ Authorization: `Bearer ${authToken}`,
244
+ }
245
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
246
+
247
+ try {
248
+ const response = await fetch(
249
+ `${BACKEND_URL}/store/guest/invoice/download/${orderId}`,
250
+ {
251
+ method: "GET",
252
+ headers: requestHeaders,
253
+ cache: "no-store",
254
+ }
255
+ )
256
+
257
+ if (!response.ok) {
258
+ const errorData = await response.json().catch(() => ({}))
259
+ throw new Error(errorData.message || "Failed to get invoice")
260
+ }
261
+
262
+ const contentType = response.headers.get("content-type")
263
+ if (contentType && contentType.includes("application/pdf")) {
264
+ const arrayBuffer = await response.arrayBuffer()
265
+ const base64 = Buffer.from(arrayBuffer).toString("base64")
266
+ return { success: true, type: "pdf", data: base64 }
267
+ } else {
268
+ const data = await response.json()
269
+ return { success: true, type: "json", data }
270
+ }
271
+ } catch (error: any) {
272
+ return { success: false, error: error.message }
273
+ }
274
+ }
275
+
276
+ export async function guestReorder(orderId: string, token?: string) {
277
+ const authHeaders = await getGuestAuthHeaders()
278
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
279
+
280
+ if (!authToken) return { success: false, error: "Unauthorized" }
281
+
282
+ const requestHeaders: Record<string, string> = {
283
+ Authorization: `Bearer ${authToken}`,
284
+ }
285
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
286
+
287
+ try {
288
+ const response = await fetch(
289
+ `${BACKEND_URL}/store/guest-orders/${orderId}/reorder`,
290
+ {
291
+ method: "POST",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ ...requestHeaders,
295
+ },
296
+ cache: "no-store",
297
+ }
298
+ )
299
+
300
+ if (!response.ok) {
301
+ const errorData = await response.json().catch(() => ({}))
302
+ throw new Error(errorData.message || "Failed to reorder")
303
+ }
304
+
305
+ const data = await response.json()
306
+ return { success: true, data }
307
+ } catch (error: any) {
308
+ return { success: false, error: error.message }
309
+ }
310
+ }
311
+
312
+ export async function createGuestReturn(
313
+ orderId: string,
314
+ items: { id: string; quantity: number }[],
315
+ reason_id?: string,
316
+ note?: string,
317
+ token?: string
318
+ ) {
319
+ const authHeaders = await getGuestAuthHeaders()
320
+ const authToken = token || (authHeaders as any).authorization?.split(" ")[1]
321
+
322
+ if (!authToken) return { success: false, error: "Unauthorized" }
323
+
324
+ const requestHeaders: Record<string, string> = {
325
+ Authorization: `Bearer ${authToken}`,
326
+ }
327
+ if (PUBLISHABLE_KEY) requestHeaders["x-publishable-api-key"] = PUBLISHABLE_KEY
328
+
329
+ try {
330
+ const response = await fetch(
331
+ `${BACKEND_URL}/store/guest-orders/${orderId}/returns`,
332
+ {
333
+ method: "POST",
334
+ headers: {
335
+ "Content-Type": "application/json",
336
+ ...requestHeaders,
337
+ },
338
+ body: JSON.stringify({
339
+ items,
340
+ reason_id,
341
+ note,
342
+ }),
343
+ cache: "no-store",
344
+ }
345
+ )
346
+
347
+ if (!response.ok) {
348
+ const errorData = await response.json().catch(() => ({}))
349
+ throw new Error(errorData.message || "Failed to create return request")
350
+ }
351
+
352
+ const data = await response.json()
353
+ return { success: true, data }
354
+ } catch (error: any) {
355
+ return { success: false, error: error.message }
356
+ }
357
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { LOGIN_VIEW } from "./types/account"
2
+ export type {
3
+ AccountPageData,
4
+ LoginSlotProps,
5
+ RegisterSlotProps,
6
+ ForgotPasswordSlotProps,
7
+ } from "./types/account"
8
+
9
+ export { default as Login } from "./components/login"
10
+ export { default as Register } from "./components/register"
11
+ export { default as ForgotPassword } from "./components/forgot-password"
12
+ export { default as LoginTemplate } from "./templates/login-template"
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+
5
+ import { LOGIN_VIEW } from "@core/types/account"
6
+ import Login from "@modules/account/components/login"
7
+ import Register from "@modules/account/components/register"
8
+ import ForgotPassword from "@modules/account/components/forgot-password"
9
+
10
+ export { LOGIN_VIEW }
11
+
12
+ const LoginTemplate = () => {
13
+ const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN)
14
+
15
+ const renderView = () => {
16
+ switch (currentView) {
17
+ case LOGIN_VIEW.REGISTER:
18
+ return <Register setCurrentView={setCurrentView} />
19
+ case LOGIN_VIEW.FORGOT_PASSWORD:
20
+ return <ForgotPassword setCurrentView={setCurrentView} />
21
+ default:
22
+ return <Login setCurrentView={setCurrentView} />
23
+ }
24
+ }
25
+
26
+ return (
27
+ <div className="min-h-screen w-full flex flex-col bg-page-bg">
28
+ <div className="flex-1 flex flex-col items-center justify-start px-2 min-[340px]:px-3 min-[550px]:px-4 sm:px-6 md:px-8 pt-2 min-[340px]:pt-3 min-[550px]:pt-4 sm:pt-4 md:pt-6 pb-6 min-[340px]:pb-8 min-[550px]:pb-8 sm:pb-8 md:pb-8">
29
+ <h1 className="text-2xl min-[340px]:text-3xl min-[550px]:text-3xl sm:text-4xl md:text-4xl font-bold text-heading mb-4 min-[340px]:mb-6 min-[550px]:mb-8 sm:mb-8 md:mb-8 text-center">
30
+ My Account
31
+ </h1>
32
+
33
+ <div
34
+ className="w-full bg-page-bg rounded-2xl shadow-lg p-4 min-[340px]:p-5 min-[550px]:p-6 sm:p-7 md:p-8"
35
+ style={{ maxWidth: "560px" }}
36
+ >
37
+ {renderView()}
38
+ </div>
39
+ </div>
40
+ </div>
41
+ )
42
+ }
43
+
44
+ export default LoginTemplate
@@ -0,0 +1,21 @@
1
+ export type AccountPageData = {
2
+ countryCode: string
3
+ }
4
+
5
+ export enum LOGIN_VIEW {
6
+ SIGN_IN = "sign-in",
7
+ REGISTER = "register",
8
+ FORGOT_PASSWORD = "forgot-password",
9
+ }
10
+
11
+ export type LoginSlotProps = {
12
+ setCurrentView: (view: LOGIN_VIEW) => void
13
+ }
14
+
15
+ export type RegisterSlotProps = {
16
+ setCurrentView: (view: LOGIN_VIEW) => void
17
+ }
18
+
19
+ export type ForgotPasswordSlotProps = {
20
+ setCurrentView: (view: LOGIN_VIEW) => void
21
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OAuth redirect_uri must exactly match a URI registered in Google Cloud Console.
3
+ * That is the storefront callback path (often includes country code), NOT the backend API path.
4
+ *
5
+ * Set NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URL to override (must match Console exactly).
6
+ */
7
+ export function getGoogleOAuthCallbackUrl(countryCode?: string): string {
8
+ const explicit = process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URL?.trim()
9
+ if (explicit) {
10
+ return explicit
11
+ }
12
+
13
+ const base =
14
+ process.env.NEXT_PUBLIC_BASE_URL ||
15
+ process.env.STOREFRONT_URL ||
16
+ "http://localhost:8000"
17
+ const normalized = base.replace(/\/$/, "")
18
+ const region =
19
+ countryCode?.trim().toLowerCase() ||
20
+ process.env.NEXT_PUBLIC_DEFAULT_REGION?.trim().toLowerCase() ||
21
+ ""
22
+
23
+ if (region && /^[a-z]{2,3}$/i.test(region)) {
24
+ return `${normalized}/${region}/auth/customer/google/callback`
25
+ }
26
+
27
+ return `${normalized}/auth/customer/google/callback`
28
+ }