create-brainerce-store 1.5.7 → 1.6.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.
@@ -1,94 +1,101 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useTranslations } from '@/lib/translations';
5
- import { cn } from '@/lib/utils';
6
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
-
8
- interface LoginFormProps {
9
- onSubmit: (email: string, password: string) => Promise<void>;
10
- error?: string | null;
11
- className?: string;
12
- }
13
-
14
- export function LoginForm({ onSubmit, error, className }: LoginFormProps) {
15
- const t = useTranslations('auth');
16
- const [email, setEmail] = useState('');
17
- const [password, setPassword] = useState('');
18
- const [loading, setLoading] = useState(false);
19
-
20
- async function handleSubmit(e: React.FormEvent) {
21
- e.preventDefault();
22
- if (loading) return;
23
-
24
- try {
25
- setLoading(true);
26
- await onSubmit(email, password);
27
- } finally {
28
- setLoading(false);
29
- }
30
- }
31
-
32
- return (
33
- <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
34
- {error && (
35
- <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
36
- {error}
37
- </div>
38
- )}
39
-
40
- <div>
41
- <label htmlFor="login-email" className="text-foreground mb-1.5 block text-sm font-medium">
42
- {t('email')}
43
- </label>
44
- <input
45
- id="login-email"
46
- type="email"
47
- required
48
- value={email}
49
- onChange={(e) => setEmail(e.target.value)}
50
- placeholder={t('emailPlaceholder')}
51
- autoComplete="email"
52
- 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"
53
- />
54
- </div>
55
-
56
- <div>
57
- <label
58
- htmlFor="login-password"
59
- className="text-foreground mb-1.5 block text-sm font-medium"
60
- >
61
- {t('password')}
62
- </label>
63
- <input
64
- id="login-password"
65
- type="password"
66
- required
67
- value={password}
68
- onChange={(e) => setPassword(e.target.value)}
69
- placeholder={t('passwordPlaceholder')}
70
- autoComplete="current-password"
71
- 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"
72
- />
73
- </div>
74
-
75
- <button
76
- type="submit"
77
- disabled={loading}
78
- 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"
79
- >
80
- {loading ? (
81
- <>
82
- <LoadingSpinner
83
- size="sm"
84
- className="border-primary-foreground/30 border-t-primary-foreground"
85
- />
86
- {t('signingIn')}
87
- </>
88
- ) : (
89
- t('signIn')
90
- )}
91
- </button>
92
- </form>
93
- );
94
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { useTranslations } from '@/lib/translations';
6
+ import { cn } from '@/lib/utils';
7
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
+
9
+ interface LoginFormProps {
10
+ onSubmit: (email: string, password: string) => Promise<void>;
11
+ error?: string | null;
12
+ className?: string;
13
+ }
14
+
15
+ export function LoginForm({ onSubmit, error, className }: LoginFormProps) {
16
+ const t = useTranslations('auth');
17
+ const [email, setEmail] = useState('');
18
+ const [password, setPassword] = useState('');
19
+ const [loading, setLoading] = useState(false);
20
+
21
+ async function handleSubmit(e: React.FormEvent) {
22
+ e.preventDefault();
23
+ if (loading) return;
24
+
25
+ try {
26
+ setLoading(true);
27
+ await onSubmit(email, password);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ }
32
+
33
+ return (
34
+ <form onSubmit={handleSubmit} className={cn('space-y-4', className)}>
35
+ {error && (
36
+ <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
37
+ {error}
38
+ </div>
39
+ )}
40
+
41
+ <div>
42
+ <label htmlFor="login-email" className="text-foreground mb-1.5 block text-sm font-medium">
43
+ {t('email')}
44
+ </label>
45
+ <input
46
+ id="login-email"
47
+ type="email"
48
+ required
49
+ value={email}
50
+ onChange={(e) => setEmail(e.target.value)}
51
+ placeholder={t('emailPlaceholder')}
52
+ autoComplete="email"
53
+ 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"
54
+ />
55
+ </div>
56
+
57
+ <div>
58
+ <label
59
+ htmlFor="login-password"
60
+ className="text-foreground mb-1.5 block text-sm font-medium"
61
+ >
62
+ {t('password')}
63
+ </label>
64
+ <input
65
+ id="login-password"
66
+ type="password"
67
+ required
68
+ value={password}
69
+ onChange={(e) => setPassword(e.target.value)}
70
+ placeholder={t('passwordPlaceholder')}
71
+ autoComplete="current-password"
72
+ 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"
73
+ />
74
+ </div>
75
+
76
+ <div className="flex justify-end">
77
+ <Link href="/forgot-password" className="text-primary text-sm hover:underline">
78
+ {t('forgotPassword')}
79
+ </Link>
80
+ </div>
81
+
82
+ <button
83
+ type="submit"
84
+ disabled={loading}
85
+ 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"
86
+ >
87
+ {loading ? (
88
+ <>
89
+ <LoadingSpinner
90
+ size="sm"
91
+ className="border-primary-foreground/30 border-t-primary-foreground"
92
+ />
93
+ {t('signingIn')}
94
+ </>
95
+ ) : (
96
+ t('signIn')
97
+ )}
98
+ </button>
99
+ </form>
100
+ );
101
+ }
@@ -120,9 +120,21 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
120
120
  }, []);
121
121
 
122
122
  const handleSdkPaymentError = useCallback((response: unknown) => {
123
+ const TRANSIENT_SDK_ERRORS = [
124
+ 'Wallet not initialized',
125
+ "SDK was not loaded as needed and therefore can't run",
126
+ ];
127
+
128
+ const msg = (response as { message?: string })?.message || '';
129
+ // Grow SDK fires transient errors during startup before its internal
130
+ // state is ready. The polling mechanism in Step 3 retries
131
+ // renderPaymentOptions() until the SDK is fully initialized.
132
+ if (TRANSIENT_SDK_ERRORS.some((e) => msg.includes(e))) {
133
+ console.info('Payment SDK: transient startup error, waiting for retry...', msg);
134
+ return;
135
+ }
123
136
  console.error('Payment SDK error:', response);
124
- const msg = (response as { message?: string })?.message || t('paymentError');
125
- setError(msg);
137
+ setError(msg || t('paymentError'));
126
138
  }, []);
127
139
 
128
140
  // Step 1: Load SDK scripts — just load, don't init yet
@@ -214,6 +226,7 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
214
226
  : paymentIntent.clientSecret;
215
227
 
216
228
  let rendered = false;
229
+ let walletReady = false;
217
230
  let pollId: ReturnType<typeof setInterval>;
218
231
 
219
232
  function tryRenderPaymentOptions() {
@@ -239,20 +252,29 @@ export function PaymentStep({ checkoutId, className }: PaymentStepProps) {
239
252
  onError: handleSdkPaymentError,
240
253
  onWalletChange: (state: string) => {
241
254
  console.info('Payment SDK wallet state:', state);
255
+ walletReady = true;
242
256
  tryRenderPaymentOptions();
243
257
  },
244
258
  },
245
259
  });
246
260
 
247
- // Poll as fallback: wait 1s for init() to finish, then poll every 500ms
261
+ // Poll as fallback but respect the SDK's readiness signal:
262
+ // - First 4s: only render if onWalletChange has fired (SDK says it's ready)
263
+ // - After 4s: force-attempt even without wallet signal (onWalletChange may not fire)
264
+ const WALLET_GRACE_PERIOD = 4000;
265
+ const initTime = Date.now();
266
+
248
267
  const timeoutId = setTimeout(() => {
249
- tryRenderPaymentOptions();
268
+ if (walletReady) tryRenderPaymentOptions();
250
269
  if (!rendered) {
251
270
  let attempts = 0;
252
- const maxAttempts = 16; // 16 * 500ms = 8 seconds after initial delay
271
+ const maxAttempts = 16; // 16 * 500ms = 8s after initial 1s delay
253
272
  pollId = setInterval(() => {
254
273
  attempts++;
255
- tryRenderPaymentOptions();
274
+ const elapsed = Date.now() - initTime;
275
+ if (walletReady || elapsed > WALLET_GRACE_PERIOD) {
276
+ tryRenderPaymentOptions();
277
+ }
256
278
  if (!rendered && attempts >= maxAttempts) {
257
279
  clearInterval(pollId);
258
280
  console.error('Payment SDK: renderPaymentOptions failed after max attempts');