@cogito.ai/cli 0.4.2 → 0.4.4
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/README.md +29 -22
- package/dist/index.js +9 -15
- package/dist/templates/web-nextjs/.github/copilot-instructions.md +5 -6
- package/dist/templates/web-nextjs/README.md +25 -24
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +1 -1
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +6 -6
- package/dist/templates/web-nextjs/apps/docs/app/docs/[[...slug]]/page.tsx +1 -6
- package/dist/templates/web-nextjs/apps/docs/app/docs/layout.tsx +1 -4
- package/dist/templates/web-nextjs/apps/docs/app/llms-full.txt/route.ts +3 -1
- package/dist/templates/web-nextjs/apps/docs/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
- package/dist/templates/web-nextjs/apps/web/.github/copilot-instructions.md +53 -6
- package/dist/templates/web-nextjs/apps/web/.github/skills/impeccable/SKILL.md +55 -0
- package/dist/templates/web-nextjs/apps/web/DESIGN.md +65 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +151 -5
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +151 -5
- package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/next.config.ts +3 -3
- package/dist/templates/web-nextjs/apps/web/package.json +4 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +167 -38
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +13 -3
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +4 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/signup/page.tsx +18 -17
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/dashboard/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +45 -2
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/profile/page.tsx +2 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/about/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/globals.css +17 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/help/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/layout.tsx +10 -8
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/page.tsx +22 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/privacy/page.tsx +3 -6
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/terms/page.tsx +1 -5
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +2 -3
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/app-sidebar.tsx +13 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/chart-area-interactive.tsx +122 -146
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/data-table.tsx +84 -149
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-documents.tsx +7 -16
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-main.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-secondary.tsx +4 -4
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-user.tsx +12 -21
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/page.tsx +10 -13
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/section-cards.tsx +5 -9
- package/dist/templates/web-nextjs/apps/web/src/components/dashboard/site-header.tsx +6 -7
- package/dist/templates/web-nextjs/apps/web/src/components/landing/features.tsx +63 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/footer.tsx +48 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/header.tsx +97 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/hero.tsx +45 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/how-it-works.tsx +35 -0
- package/dist/templates/web-nextjs/apps/web/src/components/landing/pricing-teaser.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/profile/profile-form.tsx +6 -4
- package/dist/templates/web-nextjs/apps/web/src/components/providers/theme-provider.tsx +16 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert-dialog.tsx +32 -49
- package/dist/templates/web-nextjs/apps/web/src/components/ui/alert.tsx +16 -23
- package/dist/templates/web-nextjs/apps/web/src/components/ui/avatar.tsx +25 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/badge.tsx +16 -18
- package/dist/templates/web-nextjs/apps/web/src/components/ui/breadcrumb.tsx +19 -26
- package/dist/templates/web-nextjs/apps/web/src/components/ui/button.tsx +23 -24
- package/dist/templates/web-nextjs/apps/web/src/components/ui/card.tsx +19 -36
- package/dist/templates/web-nextjs/apps/web/src/components/ui/chart.tsx +60 -94
- package/dist/templates/web-nextjs/apps/web/src/components/ui/checkbox.tsx +8 -11
- package/dist/templates/web-nextjs/apps/web/src/components/ui/collapsible.tsx +5 -17
- package/dist/templates/web-nextjs/apps/web/src/components/ui/command.tsx +25 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dialog.tsx +21 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/drawer.tsx +24 -35
- package/dist/templates/web-nextjs/apps/web/src/components/ui/dropdown-menu.tsx +26 -55
- package/dist/templates/web-nextjs/apps/web/src/components/ui/field.tsx +62 -76
- package/dist/templates/web-nextjs/apps/web/src/components/ui/form.tsx +19 -34
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input-otp.tsx +13 -20
- package/dist/templates/web-nextjs/apps/web/src/components/ui/input.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/label.tsx +6 -6
- package/dist/templates/web-nextjs/apps/web/src/components/ui/pagination.tsx +21 -42
- package/dist/templates/web-nextjs/apps/web/src/components/ui/popover.tsx +16 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/progress.tsx +5 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/radio-group.tsx +8 -8
- package/dist/templates/web-nextjs/apps/web/src/components/ui/scroll-area.tsx +10 -12
- package/dist/templates/web-nextjs/apps/web/src/components/ui/select.tsx +26 -41
- package/dist/templates/web-nextjs/apps/web/src/components/ui/separator.tsx +7 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sheet.tsx +29 -38
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sidebar.tsx +157 -189
- package/dist/templates/web-nextjs/apps/web/src/components/ui/skeleton.tsx +3 -3
- package/dist/templates/web-nextjs/apps/web/src/components/ui/slider.tsx +10 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/sonner.tsx +13 -7
- package/dist/templates/web-nextjs/apps/web/src/components/ui/switch.tsx +9 -9
- package/dist/templates/web-nextjs/apps/web/src/components/ui/table.tsx +24 -48
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tabs.tsx +21 -31
- package/dist/templates/web-nextjs/apps/web/src/components/ui/textarea.tsx +5 -5
- package/dist/templates/web-nextjs/apps/web/src/components/ui/theme-toggle.tsx +23 -0
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle-group.tsx +15 -16
- package/dist/templates/web-nextjs/apps/web/src/components/ui/toggle.tsx +14 -15
- package/dist/templates/web-nextjs/apps/web/src/components/ui/tooltip.tsx +8 -12
- package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/core/types/auth.ts +1 -3
- package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +57 -1
- package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +2 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
- package/dist/templates/web-nextjs/apps/web/src/hooks/use-mobile.ts +4 -4
- package/dist/templates/web-nextjs/apps/web/src/i18n/config.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +48 -4
- package/dist/templates/web-nextjs/apps/web/src/infra/db/client.ts +1 -4
- package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/utils.ts +2 -2
- package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +13 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
- package/dist/templates/web-nextjs/apps/web/src/styles/tokens.css +58 -0
- package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
- package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
- package/package.json +1 -1
|
@@ -12,42 +12,93 @@ import { Button } from '@/components/ui/button'
|
|
|
12
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
13
|
import { Input } from '@/components/ui/input'
|
|
14
14
|
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { sendPasswordResetOTP, resetPasswordWithOTP } from '@/features/auth'
|
|
16
|
+
import { resetPasswordWithOTPSchema, type ResetPasswordWithOTPInput } from '@/lib/validations/auth'
|
|
17
17
|
import type { ActionResult } from '@/core/types/auth'
|
|
18
18
|
|
|
19
19
|
export default function ForgotPasswordPage() {
|
|
20
20
|
const t = useTranslations('auth')
|
|
21
21
|
const routeParams = useParams<{ locale: string }>()
|
|
22
22
|
const locale = routeParams.locale ?? 'en'
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
23
|
+
const [step, setStep] = useState<'send' | 'verify'>('send')
|
|
24
|
+
const [email, setEmail] = useState('')
|
|
25
|
+
const [countdown, setCountdown] = useState(0)
|
|
25
26
|
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const {
|
|
28
|
+
register,
|
|
29
|
+
handleSubmit,
|
|
30
|
+
formState: { errors },
|
|
31
|
+
reset,
|
|
32
|
+
setValue,
|
|
33
|
+
} = useForm<ResetPasswordWithOTPInput>({
|
|
34
|
+
resolver: zodResolver(resetPasswordWithOTPSchema),
|
|
35
|
+
defaultValues: { email: '', token: '', password: '', confirmPassword: '' },
|
|
29
36
|
})
|
|
30
37
|
|
|
31
|
-
const [
|
|
32
|
-
|
|
38
|
+
const [sendState, sendAction, isSendPending] = useActionState<ActionResult | null, FormData>(
|
|
39
|
+
sendPasswordResetOTP,
|
|
33
40
|
null,
|
|
34
41
|
)
|
|
35
42
|
|
|
43
|
+
const [verifyState, verifyAction, isVerifyPending] = useActionState<
|
|
44
|
+
ActionResult | null,
|
|
45
|
+
FormData
|
|
46
|
+
>(resetPasswordWithOTP, null)
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (sendState?.error) {
|
|
50
|
+
toast.error(sendState.error)
|
|
51
|
+
} else if (sendState?.data !== undefined && sendState.error === null) {
|
|
52
|
+
toast.success('验证码已发送,请检查邮箱')
|
|
53
|
+
setStep('verify')
|
|
54
|
+
startCountdown()
|
|
55
|
+
}
|
|
56
|
+
}, [sendState])
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (verifyState?.error) {
|
|
60
|
+
toast.error(verifyState.error)
|
|
61
|
+
}
|
|
62
|
+
}, [verifyState])
|
|
63
|
+
|
|
36
64
|
useEffect(() => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
65
|
+
if (countdown > 0) {
|
|
66
|
+
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
|
|
67
|
+
return () => clearTimeout(timer)
|
|
68
|
+
}
|
|
69
|
+
return undefined
|
|
70
|
+
}, [countdown])
|
|
71
|
+
|
|
72
|
+
function startCountdown() {
|
|
73
|
+
setCountdown(60)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function onSendOTP(data: { email: string }) {
|
|
77
|
+
const formData = new FormData()
|
|
78
|
+
formData.append('email', data.email)
|
|
79
|
+
setEmail(data.email)
|
|
80
|
+
setValue('email', data.email)
|
|
81
|
+
sendAction(formData)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function onResendOTP() {
|
|
85
|
+
if (countdown > 0) return
|
|
86
|
+
const formData = new FormData()
|
|
87
|
+
formData.append('email', email)
|
|
88
|
+
sendAction(formData)
|
|
89
|
+
}
|
|
39
90
|
|
|
40
|
-
async function onSubmit(data:
|
|
91
|
+
async function onSubmit(data: ResetPasswordWithOTPInput) {
|
|
41
92
|
const formData = new FormData()
|
|
42
93
|
formData.append('email', data.email)
|
|
94
|
+
formData.append('token', data.token)
|
|
95
|
+
formData.append('password', data.password)
|
|
96
|
+
formData.append('confirmPassword', data.confirmPassword)
|
|
43
97
|
formData.append('locale', locale)
|
|
44
|
-
|
|
45
|
-
setSubmittedEmail(data.email)
|
|
46
|
-
setIsSent(true)
|
|
47
|
-
reset()
|
|
98
|
+
verifyAction(formData)
|
|
48
99
|
}
|
|
49
100
|
|
|
50
|
-
if (
|
|
101
|
+
if (step === 'send') {
|
|
51
102
|
return (
|
|
52
103
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
53
104
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
@@ -61,14 +112,38 @@ export default function ForgotPasswordPage() {
|
|
|
61
112
|
<Card>
|
|
62
113
|
<CardHeader className="text-center">
|
|
63
114
|
<CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
|
|
115
|
+
<CardDescription>{t('forgotPasswordSubtitle')}</CardDescription>
|
|
64
116
|
</CardHeader>
|
|
65
|
-
<CardContent
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
<CardContent>
|
|
118
|
+
<form onSubmit={handleSubmit(onSendOTP)}>
|
|
119
|
+
<FieldGroup>
|
|
120
|
+
<Field>
|
|
121
|
+
<FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
|
|
122
|
+
<Input
|
|
123
|
+
id="email"
|
|
124
|
+
type="email"
|
|
125
|
+
placeholder={t('emailPlaceholder')}
|
|
126
|
+
autoComplete="email"
|
|
127
|
+
{...register('email')}
|
|
128
|
+
/>
|
|
129
|
+
{errors.email && (
|
|
130
|
+
<FieldDescription className="text-destructive">
|
|
131
|
+
{errors.email.message}
|
|
132
|
+
</FieldDescription>
|
|
133
|
+
)}
|
|
134
|
+
</Field>
|
|
135
|
+
<Field>
|
|
136
|
+
<Button type="submit" className="w-full" disabled={isSendPending}>
|
|
137
|
+
{isSendPending ? '\u2026' : t('sendVerificationCode')}
|
|
138
|
+
</Button>
|
|
139
|
+
<FieldDescription className="text-center">
|
|
140
|
+
<Link href={`/${locale}/login`} className="underline underline-offset-4">
|
|
141
|
+
{t('backToLogin')}
|
|
142
|
+
</Link>
|
|
143
|
+
</FieldDescription>
|
|
144
|
+
</Field>
|
|
145
|
+
</FieldGroup>
|
|
146
|
+
</form>
|
|
72
147
|
</CardContent>
|
|
73
148
|
</Card>
|
|
74
149
|
</div>
|
|
@@ -76,6 +151,7 @@ export default function ForgotPasswordPage() {
|
|
|
76
151
|
)
|
|
77
152
|
}
|
|
78
153
|
|
|
154
|
+
// step === 'verify'
|
|
79
155
|
return (
|
|
80
156
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
|
|
81
157
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
@@ -88,8 +164,8 @@ export default function ForgotPasswordPage() {
|
|
|
88
164
|
|
|
89
165
|
<Card>
|
|
90
166
|
<CardHeader className="text-center">
|
|
91
|
-
<CardTitle className="text-xl">{t('
|
|
92
|
-
<CardDescription>{t('
|
|
167
|
+
<CardTitle className="text-xl">{t('resetPasswordTitle')}</CardTitle>
|
|
168
|
+
<CardDescription>{t('resetPasswordSubtitle')}</CardDescription>
|
|
93
169
|
</CardHeader>
|
|
94
170
|
<CardContent>
|
|
95
171
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
@@ -97,27 +173,80 @@ export default function ForgotPasswordPage() {
|
|
|
97
173
|
<FieldGroup>
|
|
98
174
|
<Field>
|
|
99
175
|
<FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
|
|
176
|
+
<Input id="email" type="email" readOnly value={email} {...register('email')} />
|
|
177
|
+
</Field>
|
|
178
|
+
<Field>
|
|
179
|
+
<FieldLabel htmlFor="token">{t('verificationCodeLabel')}</FieldLabel>
|
|
180
|
+
<Input
|
|
181
|
+
id="token"
|
|
182
|
+
type="text"
|
|
183
|
+
placeholder={t('verificationCodePlaceholder')}
|
|
184
|
+
maxLength={6}
|
|
185
|
+
{...register('token')}
|
|
186
|
+
/>
|
|
187
|
+
{errors.token && (
|
|
188
|
+
<FieldDescription className="text-destructive">
|
|
189
|
+
{errors.token.message}
|
|
190
|
+
</FieldDescription>
|
|
191
|
+
)}
|
|
192
|
+
<FieldDescription className="mt-1">
|
|
193
|
+
{countdown > 0 ? (
|
|
194
|
+
<span className="text-muted-foreground">
|
|
195
|
+
{t('resendIn', { seconds: countdown })}
|
|
196
|
+
</span>
|
|
197
|
+
) : (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={onResendOTP}
|
|
201
|
+
className="text-sm text-primary hover:underline"
|
|
202
|
+
>
|
|
203
|
+
{t('resendCode')}
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
</FieldDescription>
|
|
207
|
+
</Field>
|
|
208
|
+
<Field>
|
|
209
|
+
<FieldLabel htmlFor="password">{t('newPasswordLabel')}</FieldLabel>
|
|
210
|
+
<Input
|
|
211
|
+
id="password"
|
|
212
|
+
type="password"
|
|
213
|
+
placeholder={t('newPasswordPlaceholder')}
|
|
214
|
+
autoComplete="new-password"
|
|
215
|
+
{...register('password')}
|
|
216
|
+
/>
|
|
217
|
+
{errors.password && (
|
|
218
|
+
<FieldDescription className="text-destructive">
|
|
219
|
+
{errors.password.message}
|
|
220
|
+
</FieldDescription>
|
|
221
|
+
)}
|
|
222
|
+
</Field>
|
|
223
|
+
<Field>
|
|
224
|
+
<FieldLabel htmlFor="confirmPassword">{t('confirmPasswordLabel')}</FieldLabel>
|
|
100
225
|
<Input
|
|
101
|
-
id="
|
|
102
|
-
type="
|
|
103
|
-
placeholder={t('
|
|
104
|
-
autoComplete="
|
|
105
|
-
{...register('
|
|
226
|
+
id="confirmPassword"
|
|
227
|
+
type="password"
|
|
228
|
+
placeholder={t('confirmPasswordPlaceholder')}
|
|
229
|
+
autoComplete="new-password"
|
|
230
|
+
{...register('confirmPassword')}
|
|
106
231
|
/>
|
|
107
|
-
{errors.
|
|
232
|
+
{errors.confirmPassword && (
|
|
108
233
|
<FieldDescription className="text-destructive">
|
|
109
|
-
{errors.
|
|
234
|
+
{errors.confirmPassword.message}
|
|
110
235
|
</FieldDescription>
|
|
111
236
|
)}
|
|
112
237
|
</Field>
|
|
113
238
|
<Field>
|
|
114
|
-
<Button type="submit" className="w-full" disabled={
|
|
115
|
-
{
|
|
239
|
+
<Button type="submit" className="w-full" disabled={isVerifyPending}>
|
|
240
|
+
{isVerifyPending ? '\u2026' : t('resetPassword')}
|
|
116
241
|
</Button>
|
|
117
242
|
<FieldDescription className="text-center">
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => setStep('send')}
|
|
246
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
247
|
+
>
|
|
248
|
+
{t('changeEmail')}
|
|
249
|
+
</button>
|
|
121
250
|
</FieldDescription>
|
|
122
251
|
</Field>
|
|
123
252
|
</FieldGroup>
|
|
@@ -28,7 +28,10 @@ export default function LoginPage() {
|
|
|
28
28
|
const routeParams = useParams<{ locale: string }>()
|
|
29
29
|
const locale = routeParams.locale ?? 'en'
|
|
30
30
|
|
|
31
|
-
const {
|
|
31
|
+
const {
|
|
32
|
+
register,
|
|
33
|
+
formState: { errors },
|
|
34
|
+
} = useForm<SignInInput>({
|
|
32
35
|
resolver: zodResolver(signInSchema),
|
|
33
36
|
defaultValues: { email: '', password: '' },
|
|
34
37
|
})
|
|
@@ -41,7 +44,10 @@ export default function LoginPage() {
|
|
|
41
44
|
|
|
42
45
|
async function handleGithub() {
|
|
43
46
|
const result = await signInWithGithubForLocale(locale)
|
|
44
|
-
if (result.error) {
|
|
47
|
+
if (result.error) {
|
|
48
|
+
toast.error(result.error)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
45
51
|
if (result.data?.url) router.push(result.data.url)
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -72,7 +78,11 @@ export default function LoginPage() {
|
|
|
72
78
|
className="w-full"
|
|
73
79
|
onClick={handleGithub}
|
|
74
80
|
>
|
|
75
|
-
<svg
|
|
81
|
+
<svg
|
|
82
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
className="size-4"
|
|
85
|
+
>
|
|
76
86
|
<path
|
|
77
87
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
78
88
|
fill="currentColor"
|
|
@@ -23,7 +23,10 @@ export default function ResetPasswordPage() {
|
|
|
23
23
|
const locale = routeParams.locale ?? 'en'
|
|
24
24
|
const error = searchParams.get('error')
|
|
25
25
|
|
|
26
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
register,
|
|
28
|
+
formState: { errors },
|
|
29
|
+
} = useForm<ResetPasswordInput>({
|
|
27
30
|
resolver: zodResolver(resetPasswordSchema),
|
|
28
31
|
defaultValues: { password: '', confirmPassword: '' },
|
|
29
32
|
})
|
|
@@ -11,12 +11,7 @@ import { toast } from 'sonner'
|
|
|
11
11
|
import { Button } from '@/components/ui/button'
|
|
12
12
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
13
|
import { Input } from '@/components/ui/input'
|
|
14
|
-
import {
|
|
15
|
-
Field,
|
|
16
|
-
FieldDescription,
|
|
17
|
-
FieldGroup,
|
|
18
|
-
FieldLabel,
|
|
19
|
-
} from '@/components/ui/field'
|
|
14
|
+
import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
|
|
20
15
|
import { signUp } from '@/features/auth'
|
|
21
16
|
import { signUpSchema, type SignUpInput } from '@/lib/validations/auth'
|
|
22
17
|
import type { ActionResult } from '@/core/types/auth'
|
|
@@ -28,15 +23,18 @@ export default function SignupPage() {
|
|
|
28
23
|
const locale = routeParams.locale ?? 'en'
|
|
29
24
|
const [verifyEmail, setVerifyEmail] = useState<string | null>(null)
|
|
30
25
|
|
|
31
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
register,
|
|
28
|
+
formState: { errors },
|
|
29
|
+
} = useForm<SignUpInput>({
|
|
32
30
|
resolver: zodResolver(signUpSchema),
|
|
33
31
|
defaultValues: { email: '', password: '', confirmPassword: '' },
|
|
34
32
|
})
|
|
35
33
|
|
|
36
|
-
const [state, formAction, isPending] = useActionState<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
34
|
+
const [state, formAction, isPending] = useActionState<
|
|
35
|
+
ActionResult<SignUpSuccessData> | null,
|
|
36
|
+
FormData
|
|
37
|
+
>(signUp, null)
|
|
40
38
|
|
|
41
39
|
useEffect(() => {
|
|
42
40
|
if (state?.error) toast.error(state.error)
|
|
@@ -56,9 +54,7 @@ export default function SignupPage() {
|
|
|
56
54
|
<Card>
|
|
57
55
|
<CardHeader className="text-center">
|
|
58
56
|
<CardTitle className="text-xl">{t('verifyEmailTitle')}</CardTitle>
|
|
59
|
-
<CardDescription>
|
|
60
|
-
{t('verifyEmailMessage', { email: verifyEmail })}
|
|
61
|
-
</CardDescription>
|
|
57
|
+
<CardDescription>{t('verifyEmailMessage', { email: verifyEmail })}</CardDescription>
|
|
62
58
|
</CardHeader>
|
|
63
59
|
<CardContent>
|
|
64
60
|
<div className="text-center">
|
|
@@ -158,9 +154,14 @@ export default function SignupPage() {
|
|
|
158
154
|
</Card>
|
|
159
155
|
<FieldDescription className="px-6 text-center">
|
|
160
156
|
{t('termsText')}{' '}
|
|
161
|
-
<Link href={`/${locale}/terms`} className="underline underline-offset-4">
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
<Link href={`/${locale}/terms`} className="underline underline-offset-4">
|
|
158
|
+
{t('termsLink')}
|
|
159
|
+
</Link>{' '}
|
|
160
|
+
{t('andText')}{' '}
|
|
161
|
+
<Link href={`/${locale}/privacy`} className="underline underline-offset-4">
|
|
162
|
+
{t('privacyLink')}
|
|
163
|
+
</Link>
|
|
164
|
+
.
|
|
164
165
|
</FieldDescription>
|
|
165
166
|
</div>
|
|
166
167
|
</div>
|
|
@@ -8,11 +8,7 @@ import { getCurrentUser } from '@/features/auth/server'
|
|
|
8
8
|
|
|
9
9
|
import data from '@/components/dashboard/data.json'
|
|
10
10
|
|
|
11
|
-
export default async function DashboardPage({
|
|
12
|
-
params,
|
|
13
|
-
}: {
|
|
14
|
-
params: Promise<{ locale: string }>
|
|
15
|
-
}) {
|
|
11
|
+
export default async function DashboardPage({ params }: { params: Promise<{ locale: string }> }) {
|
|
16
12
|
const { locale } = await params
|
|
17
13
|
const user = await getCurrentUser()
|
|
18
14
|
|
package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useParams, useRouter } from 'next/navigation'
|
|
5
|
+
import { useTranslations } from 'next-intl'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { usePaymentStatus } from '@/features/subscription/hooks'
|
|
10
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
11
|
+
|
|
12
|
+
export default function PaymentStatusPage() {
|
|
13
|
+
const t = useTranslations('payment')
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const params = useParams()
|
|
16
|
+
const paymentId = params.paymentId as string
|
|
17
|
+
|
|
18
|
+
const [paymentMethod, setPaymentMethod] = useState<string>('')
|
|
19
|
+
const [orderNo, setOrderNo] = useState<string>('')
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
async function fetchPayment() {
|
|
23
|
+
const supabase = createBrowserClient(
|
|
24
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
25
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
26
|
+
)
|
|
27
|
+
const { data } = await supabase.from('payments').select('*').eq('id', paymentId).single()
|
|
28
|
+
|
|
29
|
+
if (data) {
|
|
30
|
+
setPaymentMethod(data.payment_method)
|
|
31
|
+
setOrderNo(data.order_no)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
fetchPayment()
|
|
35
|
+
}, [paymentId])
|
|
36
|
+
|
|
37
|
+
const { data: paymentStatus } = usePaymentStatus(orderNo, paymentMethod, {
|
|
38
|
+
enabled: !!orderNo && !!paymentMethod,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const status = paymentStatus?.status || 'pending'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
45
|
+
<h1 className="text-2xl font-bold mb-6">{t('statusTitle')}</h1>
|
|
46
|
+
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<CardTitle className="flex items-center gap-2">
|
|
50
|
+
{t('paymentStatus')}
|
|
51
|
+
<Badge
|
|
52
|
+
variant={
|
|
53
|
+
status === 'paid' ? 'default' : status === 'failed' ? 'destructive' : 'secondary'
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{t(status)}
|
|
57
|
+
</Badge>
|
|
58
|
+
</CardTitle>
|
|
59
|
+
</CardHeader>
|
|
60
|
+
<CardContent className="space-y-6">
|
|
61
|
+
{status === 'paid' && (
|
|
62
|
+
<div className="text-center space-y-4">
|
|
63
|
+
<div className="text-6xl">✓</div>
|
|
64
|
+
<p className="text-lg font-medium">{t('paymentSuccess')}</p>
|
|
65
|
+
<Button onClick={() => router.push('/dashboard')}>{t('goToDashboard')}</Button>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{status === 'failed' && (
|
|
70
|
+
<div className="text-center space-y-4">
|
|
71
|
+
<div className="text-6xl">✗</div>
|
|
72
|
+
<p className="text-lg font-medium">{t('paymentFailed')}</p>
|
|
73
|
+
<Button onClick={() => router.push('/pricing')}>{t('tryAgain')}</Button>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{status === 'pending' && (
|
|
78
|
+
<div className="text-center space-y-4">
|
|
79
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto" />
|
|
80
|
+
<p className="text-lg font-medium">{t('paymentPending')}</p>
|
|
81
|
+
<p className="text-sm text-muted-foreground">{t('paymentPendingDescription')}</p>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
import { useTranslations } from 'next-intl'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
8
|
+
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
9
|
+
import { Label } from '@/components/ui/label'
|
|
10
|
+
import { Input } from '@/components/ui/input'
|
|
11
|
+
import { useSubscriptionPlan } from '@/features/subscription/hooks'
|
|
12
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
13
|
+
|
|
14
|
+
export default function CheckoutPage() {
|
|
15
|
+
const t = useTranslations('payment')
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const searchParams = useSearchParams()
|
|
18
|
+
const planCode = searchParams.get('plan') || ''
|
|
19
|
+
const cycle = (searchParams.get('cycle') as 'monthly' | 'yearly') || 'monthly'
|
|
20
|
+
|
|
21
|
+
const { data: plan, isLoading } = useSubscriptionPlan(planCode)
|
|
22
|
+
const [paymentMethod, setPaymentMethod] = useState<'alipay' | 'wechat'>('alipay')
|
|
23
|
+
const [coupon, setCoupon] = useState('')
|
|
24
|
+
const [couponError, setCouponError] = useState('')
|
|
25
|
+
const [isProcessing, setIsProcessing] = useState(false)
|
|
26
|
+
|
|
27
|
+
if (!planCode) {
|
|
28
|
+
router.push('/pricing')
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
35
|
+
<div className="flex justify-center py-20">
|
|
36
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const amount = cycle === 'monthly' ? plan?.price_monthly : plan?.price_yearly
|
|
43
|
+
|
|
44
|
+
const handleSubmit = async () => {
|
|
45
|
+
setIsProcessing(true)
|
|
46
|
+
try {
|
|
47
|
+
// Validate coupon (stub)
|
|
48
|
+
if (coupon && coupon !== 'DISCOUNT10') {
|
|
49
|
+
setCouponError(t('invalidCoupon'))
|
|
50
|
+
setIsProcessing(false)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
setCouponError('')
|
|
54
|
+
|
|
55
|
+
const supabase = createBrowserClient(
|
|
56
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
57
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
58
|
+
)
|
|
59
|
+
const {
|
|
60
|
+
data: { user },
|
|
61
|
+
} = await supabase.auth.getUser()
|
|
62
|
+
if (!user) {
|
|
63
|
+
router.push('/login')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create payment record
|
|
68
|
+
const res = await fetch('/api/payments/create', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
userId: user.id,
|
|
73
|
+
planId: plan?.id,
|
|
74
|
+
amount,
|
|
75
|
+
paymentMethod,
|
|
76
|
+
billingCycle: cycle,
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!res.ok) throw new Error('Failed to create payment')
|
|
81
|
+
const payment = await res.json()
|
|
82
|
+
|
|
83
|
+
// Initiate payment
|
|
84
|
+
if (paymentMethod === 'alipay') {
|
|
85
|
+
const { initiateAlipayPayment } = await import('@/features/subscription/alipay/client')
|
|
86
|
+
await initiateAlipayPayment(payment.id, user.id)
|
|
87
|
+
} else {
|
|
88
|
+
const { initiateWechatPayment } = await import('@/features/subscription/wechat/client')
|
|
89
|
+
const result = await initiateWechatPayment(payment.id, user.id)
|
|
90
|
+
if (result.type === 'qrcode') {
|
|
91
|
+
router.push(`/payment/${payment.id}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
setCouponError(t('paymentError'))
|
|
96
|
+
} finally {
|
|
97
|
+
setIsProcessing(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
103
|
+
<h1 className="text-2xl font-bold mb-6">{t('checkoutTitle')}</h1>
|
|
104
|
+
|
|
105
|
+
<Card className="mb-6">
|
|
106
|
+
<CardHeader>
|
|
107
|
+
<CardTitle>{t('orderSummary')}</CardTitle>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent className="space-y-4">
|
|
110
|
+
<div className="flex justify-between">
|
|
111
|
+
<span>{t('plan')}</span>
|
|
112
|
+
<span className="font-medium">{plan?.plan_name}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex justify-between">
|
|
115
|
+
<span>{t('billingCycle')}</span>
|
|
116
|
+
<span className="font-medium">{cycle === 'monthly' ? t('monthly') : t('yearly')}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex justify-between text-lg font-bold">
|
|
119
|
+
<span>{t('total')}</span>
|
|
120
|
+
<span>¥{((amount || 0) / 100).toFixed(2)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
|
|
125
|
+
<Card className="mb-6">
|
|
126
|
+
<CardHeader>
|
|
127
|
+
<CardTitle>{t('paymentMethod')}</CardTitle>
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent>
|
|
130
|
+
<RadioGroup
|
|
131
|
+
value={paymentMethod}
|
|
132
|
+
onValueChange={(v) => setPaymentMethod(v as 'alipay' | 'wechat')}
|
|
133
|
+
className="space-y-3"
|
|
134
|
+
>
|
|
135
|
+
<div className="flex items-center space-x-2 border rounded-lg p-4">
|
|
136
|
+
<RadioGroupItem value="alipay" id="alipay" />
|
|
137
|
+
<Label htmlFor="alipay" className="flex-1 cursor-pointer">
|
|
138
|
+
{t('alipay')}
|
|
139
|
+
</Label>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center space-x-2 border rounded-lg p-4">
|
|
142
|
+
<RadioGroupItem value="wechat" id="wechat" />
|
|
143
|
+
<Label htmlFor="wechat" className="flex-1 cursor-pointer">
|
|
144
|
+
{t('wechatPay')}
|
|
145
|
+
</Label>
|
|
146
|
+
</div>
|
|
147
|
+
</RadioGroup>
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
|
|
151
|
+
<Card className="mb-6">
|
|
152
|
+
<CardHeader>
|
|
153
|
+
<CardTitle>{t('coupon')}</CardTitle>
|
|
154
|
+
</CardHeader>
|
|
155
|
+
<CardContent>
|
|
156
|
+
<Input
|
|
157
|
+
placeholder={t('couponPlaceholder')}
|
|
158
|
+
value={coupon}
|
|
159
|
+
onChange={(e) => setCoupon(e.target.value)}
|
|
160
|
+
/>
|
|
161
|
+
{couponError && <p className="text-sm text-destructive mt-2">{couponError}</p>}
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
|
|
165
|
+
<Button className="w-full" size="lg" onClick={handleSubmit} disabled={isProcessing}>
|
|
166
|
+
{isProcessing ? t('processing') : t('confirmPayment')}
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|