create-brainerce-store 1.18.0 → 1.20.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/LICENSE +0 -0
- package/dist/index.js +31 -9
- package/messages/en.json +366 -362
- package/messages/he.json +366 -362
- package/package.json +8 -8
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
- package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
- package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/account/page.tsx +122 -122
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
- package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
- package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
- package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
- package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
- package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/login/page.tsx +59 -59
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +17 -0
- package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
- package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
- package/templates/nextjs/base/src/app/products/page.tsx +431 -431
- package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
- package/templates/nextjs/base/src/app/register/page.tsx +65 -65
- package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
- package/templates/nextjs/base/src/app/robots.ts +14 -14
- package/templates/nextjs/base/src/app/sitemap.ts +25 -25
- package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
- package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
- package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
- package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +49 -3
- package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
- package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
- package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
- package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
- package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
- package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
- package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
- package/templates/nextjs/base/src/lib/auth.ts +149 -149
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
- package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
- package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
- package/templates/nextjs/base/src/lib/translations.ts +0 -11
- package/templates/nextjs/base/src/middleware.ts +0 -25
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import Link from 'next/link';
|
|
5
|
-
import { getClient } from '@/lib/brainerce';
|
|
6
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
-
import { useTranslations } from '@/lib/translations';
|
|
8
|
-
|
|
9
|
-
export default function ForgotPasswordPage() {
|
|
10
|
-
const t = useTranslations('auth');
|
|
11
|
-
const [email, setEmail] = useState('');
|
|
12
|
-
const [loading, setLoading] = useState(false);
|
|
13
|
-
const [sent, setSent] = useState(false);
|
|
14
|
-
const [error, setError] = useState<string | null>(null);
|
|
15
|
-
|
|
16
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
17
|
-
e.preventDefault();
|
|
18
|
-
if (loading) return;
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
setLoading(true);
|
|
22
|
-
setError(null);
|
|
23
|
-
const client = getClient();
|
|
24
|
-
const resetUrl = `${window.location.origin}/api/auth/reset-callback`;
|
|
25
|
-
await client.forgotPassword(email, { resetUrl });
|
|
26
|
-
setSent(true);
|
|
27
|
-
} catch (err) {
|
|
28
|
-
const message =
|
|
29
|
-
err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
30
|
-
setError(message);
|
|
31
|
-
} finally {
|
|
32
|
-
setLoading(false);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
38
|
-
<div className="w-full max-w-md space-y-6">
|
|
39
|
-
<div className="text-center">
|
|
40
|
-
<h1 className="text-foreground text-2xl font-bold">{t('forgotPasswordTitle')}</h1>
|
|
41
|
-
<p className="text-muted-foreground mt-1 text-sm">{t('forgotPasswordSubtitle')}</p>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
{error && (
|
|
45
|
-
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
|
|
46
|
-
{error}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
|
-
|
|
50
|
-
{sent ? (
|
|
51
|
-
<div className="space-y-4">
|
|
52
|
-
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
|
|
53
|
-
{t('resetLinkSent')}
|
|
54
|
-
</div>
|
|
55
|
-
<Link
|
|
56
|
-
href="/login"
|
|
57
|
-
className="text-primary block text-center text-sm font-medium hover:underline"
|
|
58
|
-
>
|
|
59
|
-
{t('backToLogin')}
|
|
60
|
-
</Link>
|
|
61
|
-
</div>
|
|
62
|
-
) : (
|
|
63
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
64
|
-
<div>
|
|
65
|
-
<label
|
|
66
|
-
htmlFor="forgot-email"
|
|
67
|
-
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
68
|
-
>
|
|
69
|
-
{t('email')}
|
|
70
|
-
</label>
|
|
71
|
-
<input
|
|
72
|
-
id="forgot-email"
|
|
73
|
-
type="email"
|
|
74
|
-
required
|
|
75
|
-
value={email}
|
|
76
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
77
|
-
placeholder={t('emailPlaceholder')}
|
|
78
|
-
autoComplete="email"
|
|
79
|
-
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
80
|
-
/>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<button
|
|
84
|
-
type="submit"
|
|
85
|
-
disabled={loading}
|
|
86
|
-
className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
87
|
-
>
|
|
88
|
-
{loading ? (
|
|
89
|
-
<>
|
|
90
|
-
<LoadingSpinner
|
|
91
|
-
size="sm"
|
|
92
|
-
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
93
|
-
/>
|
|
94
|
-
{t('sendingResetLink')}
|
|
95
|
-
</>
|
|
96
|
-
) : (
|
|
97
|
-
t('sendResetLink')
|
|
98
|
-
)}
|
|
99
|
-
</button>
|
|
100
|
-
|
|
101
|
-
<Link
|
|
102
|
-
href="/login"
|
|
103
|
-
className="text-muted-foreground block text-center text-sm hover:underline"
|
|
104
|
-
>
|
|
105
|
-
{t('backToLogin')}
|
|
106
|
-
</Link>
|
|
107
|
-
</form>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { getClient } from '@/lib/brainerce';
|
|
6
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
+
import { useTranslations } from '@/lib/translations';
|
|
8
|
+
|
|
9
|
+
export default function ForgotPasswordPage() {
|
|
10
|
+
const t = useTranslations('auth');
|
|
11
|
+
const [email, setEmail] = useState('');
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [sent, setSent] = useState(false);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
if (loading) return;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
const client = getClient();
|
|
24
|
+
const resetUrl = `${window.location.origin}/api/auth/reset-callback`;
|
|
25
|
+
await client.forgotPassword(email, { resetUrl });
|
|
26
|
+
setSent(true);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const message =
|
|
29
|
+
err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
30
|
+
setError(message);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
38
|
+
<div className="w-full max-w-md space-y-6">
|
|
39
|
+
<div className="text-center">
|
|
40
|
+
<h1 className="text-foreground text-2xl font-bold">{t('forgotPasswordTitle')}</h1>
|
|
41
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('forgotPasswordSubtitle')}</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{error && (
|
|
45
|
+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
|
|
46
|
+
{error}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{sent ? (
|
|
51
|
+
<div className="space-y-4">
|
|
52
|
+
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
|
|
53
|
+
{t('resetLinkSent')}
|
|
54
|
+
</div>
|
|
55
|
+
<Link
|
|
56
|
+
href="/login"
|
|
57
|
+
className="text-primary block text-center text-sm font-medium hover:underline"
|
|
58
|
+
>
|
|
59
|
+
{t('backToLogin')}
|
|
60
|
+
</Link>
|
|
61
|
+
</div>
|
|
62
|
+
) : (
|
|
63
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
64
|
+
<div>
|
|
65
|
+
<label
|
|
66
|
+
htmlFor="forgot-email"
|
|
67
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
68
|
+
>
|
|
69
|
+
{t('email')}
|
|
70
|
+
</label>
|
|
71
|
+
<input
|
|
72
|
+
id="forgot-email"
|
|
73
|
+
type="email"
|
|
74
|
+
required
|
|
75
|
+
value={email}
|
|
76
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
77
|
+
placeholder={t('emailPlaceholder')}
|
|
78
|
+
autoComplete="email"
|
|
79
|
+
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={loading}
|
|
86
|
+
className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
87
|
+
>
|
|
88
|
+
{loading ? (
|
|
89
|
+
<>
|
|
90
|
+
<LoadingSpinner
|
|
91
|
+
size="sm"
|
|
92
|
+
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
93
|
+
/>
|
|
94
|
+
{t('sendingResetLink')}
|
|
95
|
+
</>
|
|
96
|
+
) : (
|
|
97
|
+
t('sendResetLink')
|
|
98
|
+
)}
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
<Link
|
|
102
|
+
href="/login"
|
|
103
|
+
className="text-muted-foreground block text-center text-sm hover:underline"
|
|
104
|
+
>
|
|
105
|
+
{t('backToLogin')}
|
|
106
|
+
</Link>
|
|
107
|
+
</form>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
<%- fontImport %>
|
|
4
|
+
import { StoreProvider } from '@/providers/store-provider';
|
|
5
|
+
import { Header } from '@/components/layout/header';
|
|
6
|
+
import { Footer } from '@/components/layout/footer';
|
|
7
|
+
import { getDirection, supportedLocales } from '@/i18n';
|
|
8
|
+
import '../globals.css';
|
|
9
|
+
|
|
10
|
+
<%- fontVariable %>
|
|
11
|
+
|
|
12
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = {
|
|
15
|
+
metadataBase: new URL(baseUrl),
|
|
16
|
+
title: {
|
|
17
|
+
default: '<%= storeName %>',
|
|
18
|
+
template: `%s | <%= storeName %>`,
|
|
19
|
+
},
|
|
20
|
+
description: '<%= storeName %>',
|
|
21
|
+
alternates: {
|
|
22
|
+
canonical: '/',
|
|
23
|
+
},
|
|
24
|
+
openGraph: {
|
|
25
|
+
siteName: '<%= storeName %>',
|
|
26
|
+
type: 'website',
|
|
27
|
+
},
|
|
28
|
+
robots: {
|
|
29
|
+
index: true,
|
|
30
|
+
follow: true,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const organizationJsonLd = {
|
|
35
|
+
'@context': 'https://schema.org',
|
|
36
|
+
'@type': 'Organization',
|
|
37
|
+
name: '<%= storeName %>',
|
|
38
|
+
url: baseUrl,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function generateStaticParams() {
|
|
42
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function RootLayout({
|
|
46
|
+
children,
|
|
47
|
+
params,
|
|
48
|
+
}: {
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
params: { locale: string };
|
|
51
|
+
}) {
|
|
52
|
+
const dir = getDirection(params.locale);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<html lang={params.locale} dir={dir}>
|
|
56
|
+
<head>
|
|
57
|
+
<script
|
|
58
|
+
type="application/ld+json"
|
|
59
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationJsonLd) }}
|
|
60
|
+
/>
|
|
61
|
+
</head>
|
|
62
|
+
<body className={font.className}>
|
|
63
|
+
<StoreProvider locale={params.locale}>
|
|
64
|
+
<div className="min-h-screen flex flex-col">
|
|
65
|
+
<Header />
|
|
66
|
+
<main className="flex-1">{children}</main>
|
|
67
|
+
<Footer />
|
|
68
|
+
</div>
|
|
69
|
+
</StoreProvider>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
<% } else { %>
|
|
1
75
|
import type { Metadata } from 'next';
|
|
2
76
|
<%- fontImport %>
|
|
3
77
|
import { StoreProvider } from '@/providers/store-provider';
|
|
@@ -62,3 +136,4 @@ export default function RootLayout({
|
|
|
62
136
|
</html>
|
|
63
137
|
);
|
|
64
138
|
}
|
|
139
|
+
<% } %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
|
|
3
|
-
export const metadata: Metadata = {
|
|
4
|
-
robots: { index: false, follow: false },
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
8
|
-
return <>{children}</>;
|
|
9
|
-
}
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
robots: { index: false, follow: false },
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
8
|
+
return <>{children}</>;
|
|
9
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import Link from 'next/link';
|
|
6
|
-
import { useAuth } from '@/providers/store-provider';
|
|
7
|
-
import { proxyLogin } from '@/lib/auth';
|
|
8
|
-
import { LoginForm } from '@/components/auth/login-form';
|
|
9
|
-
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
10
|
-
import { useTranslations } from '@/lib/translations';
|
|
11
|
-
|
|
12
|
-
export default function LoginPage() {
|
|
13
|
-
const router = useRouter();
|
|
14
|
-
const auth = useAuth();
|
|
15
|
-
const t = useTranslations('auth');
|
|
16
|
-
const [error, setError] = useState<string | null>(null);
|
|
17
|
-
|
|
18
|
-
async function handleLogin(email: string, password: string) {
|
|
19
|
-
try {
|
|
20
|
-
setError(null);
|
|
21
|
-
const result = await proxyLogin(email, password);
|
|
22
|
-
|
|
23
|
-
if (result.requiresVerification) {
|
|
24
|
-
// Verification token is NOT the auth JWT — safe to pass in URL
|
|
25
|
-
router.push('/verify-email');
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Cookie was set by the proxy; refresh auth state
|
|
30
|
-
await auth.login();
|
|
31
|
-
router.push('/');
|
|
32
|
-
} catch (err) {
|
|
33
|
-
const message = err instanceof Error ? err.message : 'Login failed. Please try again.';
|
|
34
|
-
setError(message);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
40
|
-
<div className="w-full max-w-md space-y-6">
|
|
41
|
-
<div className="text-center">
|
|
42
|
-
<h1 className="text-foreground text-2xl font-bold">{t('welcomeBack')}</h1>
|
|
43
|
-
<p className="text-muted-foreground mt-1 text-sm">{t('signInSubtitle')}</p>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<LoginForm onSubmit={handleLogin} error={error} />
|
|
47
|
-
|
|
48
|
-
<OAuthButtons />
|
|
49
|
-
|
|
50
|
-
<p className="text-muted-foreground text-center text-sm">
|
|
51
|
-
{t('noAccount')}{' '}
|
|
52
|
-
<Link href="/register" className="text-primary font-medium hover:underline">
|
|
53
|
-
{t('createOne')}
|
|
54
|
-
</Link>
|
|
55
|
-
</p>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { useAuth } from '@/providers/store-provider';
|
|
7
|
+
import { proxyLogin } from '@/lib/auth';
|
|
8
|
+
import { LoginForm } from '@/components/auth/login-form';
|
|
9
|
+
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
10
|
+
import { useTranslations } from '@/lib/translations';
|
|
11
|
+
|
|
12
|
+
export default function LoginPage() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const auth = useAuth();
|
|
15
|
+
const t = useTranslations('auth');
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
async function handleLogin(email: string, password: string) {
|
|
19
|
+
try {
|
|
20
|
+
setError(null);
|
|
21
|
+
const result = await proxyLogin(email, password);
|
|
22
|
+
|
|
23
|
+
if (result.requiresVerification) {
|
|
24
|
+
// Verification token is NOT the auth JWT — safe to pass in URL
|
|
25
|
+
router.push('/verify-email');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Cookie was set by the proxy; refresh auth state
|
|
30
|
+
await auth.login();
|
|
31
|
+
router.push('/');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const message = err instanceof Error ? err.message : 'Login failed. Please try again.';
|
|
34
|
+
setError(message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
40
|
+
<div className="w-full max-w-md space-y-6">
|
|
41
|
+
<div className="text-center">
|
|
42
|
+
<h1 className="text-foreground text-2xl font-bold">{t('welcomeBack')}</h1>
|
|
43
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('signInSubtitle')}</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<LoginForm onSubmit={handleLogin} error={error} />
|
|
47
|
+
|
|
48
|
+
<OAuthButtons />
|
|
49
|
+
|
|
50
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
51
|
+
{t('noAccount')}{' '}
|
|
52
|
+
<Link href="/register" className="text-primary font-medium hover:underline">
|
|
53
|
+
{t('createOne')}
|
|
54
|
+
</Link>
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Metadata } from 'next';
|
|
2
|
-
|
|
3
|
-
export const metadata: Metadata = {
|
|
4
|
-
robots: { index: false, follow: false },
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
8
|
-
return <>{children}</>;
|
|
9
|
-
}
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
robots: { index: false, follow: false },
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
8
|
+
return <>{children}</>;
|
|
9
|
+
}
|
|
@@ -35,6 +35,23 @@ function OrderConfirmationContent() {
|
|
|
35
35
|
client.handlePaymentSuccess(checkoutId!);
|
|
36
36
|
await refreshCart();
|
|
37
37
|
|
|
38
|
+
// For redirect-based payment providers (e.g. CardCom), the customer
|
|
39
|
+
// returns with provider params in the URL (lowprofilecode, etc.).
|
|
40
|
+
// Send these to the backend for server-side verification via the
|
|
41
|
+
// provider's API (e.g. GetLpResult) — never trust URL params alone.
|
|
42
|
+
const lowProfileCode =
|
|
43
|
+
searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
|
|
44
|
+
if (lowProfileCode) {
|
|
45
|
+
try {
|
|
46
|
+
await client.confirmSdkPayment(checkoutId!, {
|
|
47
|
+
paymentIntentId: lowProfileCode,
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn('Redirect payment confirmation failed:', err);
|
|
51
|
+
// Don't block — webhook may still process the payment
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
const orderResult = await client.waitForOrder(checkoutId!, {
|
|
39
56
|
maxWaitMs: 30000,
|
|
40
57
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Suspense } from 'react';
|
|
6
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lightweight callback page for iframe-based payment providers (e.g. CardCom).
|
|
10
|
+
*
|
|
11
|
+
* After the customer pays on the provider's hosted page (rendered inside an
|
|
12
|
+
* iframe on the checkout page), the provider redirects *inside the iframe* to
|
|
13
|
+
* this page. We extract the relevant query params and send them to the parent
|
|
14
|
+
* window via postMessage so the checkout page can verify the payment
|
|
15
|
+
* server-side and proceed to order confirmation.
|
|
16
|
+
*/
|
|
17
|
+
function PaymentCompleteContent() {
|
|
18
|
+
const searchParams = useSearchParams();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Only send postMessage when running inside an iframe
|
|
22
|
+
if (window.parent === window) {
|
|
23
|
+
// Not in iframe — fallback: redirect to order-confirmation directly
|
|
24
|
+
const checkoutId = searchParams.get('checkout_id');
|
|
25
|
+
if (checkoutId) {
|
|
26
|
+
window.location.href = `/order-confirmation?${searchParams.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Collect all query params from the provider redirect
|
|
32
|
+
const data: Record<string, string> = {};
|
|
33
|
+
searchParams.forEach((value, key) => {
|
|
34
|
+
data[key] = value;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
window.parent.postMessage({ type: 'brainerce:payment-complete', data }, window.location.origin);
|
|
38
|
+
}, [searchParams]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex min-h-[200px] items-center justify-center">
|
|
42
|
+
<LoadingSpinner size="lg" />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function PaymentCompletePage() {
|
|
48
|
+
return (
|
|
49
|
+
<Suspense
|
|
50
|
+
fallback={
|
|
51
|
+
<div className="flex min-h-[200px] items-center justify-center">
|
|
52
|
+
<LoadingSpinner size="lg" />
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<PaymentCompleteContent />
|
|
57
|
+
</Suspense>
|
|
58
|
+
);
|
|
59
|
+
}
|