@open-mercato/onboarding 0.4.2-canary-c02407ff85
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/build.mjs +62 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/onboarding/acl.js +11 -0
- package/dist/modules/onboarding/acl.js.map +7 -0
- package/dist/modules/onboarding/api/get/onboarding/verify.js +208 -0
- package/dist/modules/onboarding/api/get/onboarding/verify.js.map +7 -0
- package/dist/modules/onboarding/api/post/onboarding.js +193 -0
- package/dist/modules/onboarding/api/post/onboarding.js.map +7 -0
- package/dist/modules/onboarding/data/entities.js +84 -0
- package/dist/modules/onboarding/data/entities.js.map +7 -0
- package/dist/modules/onboarding/data/validators.js +27 -0
- package/dist/modules/onboarding/data/validators.js.map +7 -0
- package/dist/modules/onboarding/emails/AdminNotificationEmail.js +18 -0
- package/dist/modules/onboarding/emails/AdminNotificationEmail.js.map +7 -0
- package/dist/modules/onboarding/emails/VerificationEmail.js +36 -0
- package/dist/modules/onboarding/emails/VerificationEmail.js.map +7 -0
- package/dist/modules/onboarding/frontend/onboarding/page.js +279 -0
- package/dist/modules/onboarding/frontend/onboarding/page.js.map +7 -0
- package/dist/modules/onboarding/index.js +14 -0
- package/dist/modules/onboarding/index.js.map +7 -0
- package/dist/modules/onboarding/lib/service.js +83 -0
- package/dist/modules/onboarding/lib/service.js.map +7 -0
- package/dist/modules/onboarding/migrations/Migration20260112142945.js +12 -0
- package/dist/modules/onboarding/migrations/Migration20260112142945.js.map +7 -0
- package/generated/entities/onboarding_request/index.ts +19 -0
- package/generated/entities.ids.generated.ts +11 -0
- package/generated/entity-fields-registry.ts +11 -0
- package/jest.config.cjs +19 -0
- package/package.json +83 -0
- package/src/index.ts +2 -0
- package/src/modules/onboarding/acl.ts +7 -0
- package/src/modules/onboarding/api/get/onboarding/verify.ts +224 -0
- package/src/modules/onboarding/api/post/onboarding.ts +210 -0
- package/src/modules/onboarding/data/entities.ts +67 -0
- package/src/modules/onboarding/data/validators.ts +27 -0
- package/src/modules/onboarding/emails/AdminNotificationEmail.tsx +32 -0
- package/src/modules/onboarding/emails/VerificationEmail.tsx +54 -0
- package/src/modules/onboarding/frontend/onboarding/page.tsx +305 -0
- package/src/modules/onboarding/i18n/de.json +49 -0
- package/src/modules/onboarding/i18n/en.json +49 -0
- package/src/modules/onboarding/i18n/es.json +49 -0
- package/src/modules/onboarding/i18n/pl.json +49 -0
- package/src/modules/onboarding/index.ts +12 -0
- package/src/modules/onboarding/lib/service.ts +90 -0
- package/src/modules/onboarding/migrations/.snapshot-open-mercato.json +230 -0
- package/src/modules/onboarding/migrations/Migration20260112142945.ts +11 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const onboardingStartSchema = z.object({
|
|
4
|
+
email: z.string().email(),
|
|
5
|
+
firstName: z.string().min(1).max(120),
|
|
6
|
+
lastName: z.string().min(1).max(120),
|
|
7
|
+
organizationName: z.string().min(1).max(240),
|
|
8
|
+
password: z.string().min(6).max(120),
|
|
9
|
+
confirmPassword: z.string().min(6).max(120),
|
|
10
|
+
termsAccepted: z.literal(true),
|
|
11
|
+
locale: z.string().min(2).max(10).optional(),
|
|
12
|
+
}).superRefine((value, ctx) => {
|
|
13
|
+
if (value.password !== value.confirmPassword) {
|
|
14
|
+
ctx.addIssue({
|
|
15
|
+
code: z.ZodIssueCode.custom,
|
|
16
|
+
message: 'Passwords must match.',
|
|
17
|
+
path: ['confirmPassword'],
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const onboardingVerifySchema = z.object({
|
|
23
|
+
token: z.string().min(32),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type OnboardingStartInput = z.infer<typeof onboardingStartSchema>
|
|
27
|
+
export type OnboardingVerifyInput = z.infer<typeof onboardingVerifySchema>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Html, Head, Preview, Body, Container, Heading, Text, Hr } from '@react-email/components'
|
|
3
|
+
|
|
4
|
+
export type AdminNotificationCopy = {
|
|
5
|
+
preview: string
|
|
6
|
+
heading: string
|
|
7
|
+
body: string
|
|
8
|
+
footer: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type AdminNotificationEmailProps = {
|
|
12
|
+
copy: AdminNotificationCopy
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function AdminNotificationEmail({ copy }: AdminNotificationEmailProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Html>
|
|
18
|
+
<Head>
|
|
19
|
+
<title>{copy.heading}</title>
|
|
20
|
+
</Head>
|
|
21
|
+
<Preview>{copy.preview}</Preview>
|
|
22
|
+
<Body style={{ backgroundColor: '#f8fafc', fontFamily: 'Helvetica, Arial, sans-serif', padding: '24px 0' }}>
|
|
23
|
+
<Container style={{ backgroundColor: '#ffffff', padding: '28px', borderRadius: '12px', margin: '0 auto', maxWidth: '520px' }}>
|
|
24
|
+
<Heading style={{ fontSize: '22px', fontWeight: 600, margin: '0 0 16px', color: '#0f172a' }}>{copy.heading}</Heading>
|
|
25
|
+
<Text style={{ fontSize: '15px', color: '#1f2937', lineHeight: '24px', marginBottom: '20px' }}>{copy.body}</Text>
|
|
26
|
+
<Hr style={{ borderColor: '#e2e8f0', margin: '24px 0' }} />
|
|
27
|
+
<Text style={{ fontSize: '13px', color: '#64748b' }}>{copy.footer}</Text>
|
|
28
|
+
</Container>
|
|
29
|
+
</Body>
|
|
30
|
+
</Html>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Html, Head, Preview, Body, Container, Heading, Text, Section, Button, Hr } from '@react-email/components'
|
|
3
|
+
|
|
4
|
+
export type VerificationEmailCopy = {
|
|
5
|
+
preview: string
|
|
6
|
+
heading: string
|
|
7
|
+
greeting: string
|
|
8
|
+
body: string
|
|
9
|
+
cta: string
|
|
10
|
+
expiry: string
|
|
11
|
+
footer: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type VerificationEmailProps = {
|
|
15
|
+
verifyUrl: string
|
|
16
|
+
copy: VerificationEmailCopy
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function VerificationEmail({ verifyUrl, copy }: VerificationEmailProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Html>
|
|
22
|
+
<Head>
|
|
23
|
+
<title>{copy.heading}</title>
|
|
24
|
+
</Head>
|
|
25
|
+
<Preview>{copy.preview}</Preview>
|
|
26
|
+
<Body style={{ backgroundColor: '#f1f5f9', fontFamily: 'Helvetica, Arial, sans-serif', padding: '24px 0' }}>
|
|
27
|
+
<Container style={{ backgroundColor: '#ffffff', padding: '32px', borderRadius: '12px', margin: '0 auto', maxWidth: '520px' }}>
|
|
28
|
+
<Heading style={{ fontSize: '24px', fontWeight: 600, margin: '0 0 16px' }}>{copy.heading}</Heading>
|
|
29
|
+
<Text style={{ fontSize: '16px', color: '#334155', marginBottom: '16px' }}>{copy.greeting}</Text>
|
|
30
|
+
<Text style={{ fontSize: '16px', color: '#334155', marginBottom: '16px', lineHeight: '24px' }}>{copy.body}</Text>
|
|
31
|
+
<Section style={{ textAlign: 'center', margin: '32px 0' }}>
|
|
32
|
+
<Button
|
|
33
|
+
href={verifyUrl}
|
|
34
|
+
style={{
|
|
35
|
+
backgroundColor: '#111827',
|
|
36
|
+
color: '#ffffff',
|
|
37
|
+
padding: '12px 24px',
|
|
38
|
+
borderRadius: '8px',
|
|
39
|
+
fontSize: '15px',
|
|
40
|
+
textDecoration: 'none',
|
|
41
|
+
display: 'inline-block',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{copy.cta}
|
|
45
|
+
</Button>
|
|
46
|
+
</Section>
|
|
47
|
+
<Text style={{ fontSize: '14px', color: '#64748b', marginBottom: '16px', lineHeight: '22px' }}>{copy.expiry}</Text>
|
|
48
|
+
<Hr style={{ borderColor: '#e2e8f0', margin: '24px 0' }} />
|
|
49
|
+
<Text style={{ fontSize: '12px', color: '#94a3b8' }}>{copy.footer}</Text>
|
|
50
|
+
</Container>
|
|
51
|
+
</Body>
|
|
52
|
+
</Html>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from 'next/image'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@open-mercato/ui/primitives/card'
|
|
6
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
8
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
9
|
+
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
10
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
11
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
12
|
+
import { onboardingStartSchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'
|
|
13
|
+
|
|
14
|
+
type SubmissionState = 'idle' | 'loading' | 'success'
|
|
15
|
+
type FieldErrors = Partial<Record<
|
|
16
|
+
'email' | 'firstName' | 'lastName' | 'organizationName' | 'password' | 'confirmPassword' | 'termsAccepted',
|
|
17
|
+
string
|
|
18
|
+
>>
|
|
19
|
+
|
|
20
|
+
export default function OnboardingPage() {
|
|
21
|
+
const t = useT()
|
|
22
|
+
const translate = (key: string, fallback: string, params?: Record<string, string | number>) =>
|
|
23
|
+
translateWithFallback(t, key, fallback, params)
|
|
24
|
+
const locale = useLocale()
|
|
25
|
+
const [state, setState] = useState<SubmissionState>('idle')
|
|
26
|
+
const [globalError, setGlobalError] = useState<string | null>(null)
|
|
27
|
+
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
|
|
28
|
+
const [termsAccepted, setTermsAccepted] = useState(false)
|
|
29
|
+
const [emailSubmitted, setEmailSubmitted] = useState<string | null>(null)
|
|
30
|
+
|
|
31
|
+
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
setState('loading')
|
|
34
|
+
setGlobalError(null)
|
|
35
|
+
setFieldErrors({})
|
|
36
|
+
|
|
37
|
+
const form = new FormData(event.currentTarget)
|
|
38
|
+
const payload = {
|
|
39
|
+
email: String(form.get('email') ?? '').trim(),
|
|
40
|
+
firstName: String(form.get('firstName') ?? '').trim(),
|
|
41
|
+
lastName: String(form.get('lastName') ?? '').trim(),
|
|
42
|
+
organizationName: String(form.get('organizationName') ?? '').trim(),
|
|
43
|
+
password: String(form.get('password') ?? ''),
|
|
44
|
+
confirmPassword: String(form.get('confirmPassword') ?? ''),
|
|
45
|
+
termsAccepted: termsAccepted,
|
|
46
|
+
locale,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsed = onboardingStartSchema.safeParse(payload)
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
const issueMap: FieldErrors = {}
|
|
52
|
+
parsed.error.issues.forEach((issue) => {
|
|
53
|
+
const path = issue.path[0]
|
|
54
|
+
if (!path) return
|
|
55
|
+
switch (path) {
|
|
56
|
+
case 'email':
|
|
57
|
+
issueMap.email = translate('onboarding.errors.emailInvalid', 'Enter a valid work email.')
|
|
58
|
+
break
|
|
59
|
+
case 'firstName':
|
|
60
|
+
issueMap.firstName = translate('onboarding.errors.firstNameRequired', 'First name is required.')
|
|
61
|
+
break
|
|
62
|
+
case 'lastName':
|
|
63
|
+
issueMap.lastName = translate('onboarding.errors.lastNameRequired', 'Last name is required.')
|
|
64
|
+
break
|
|
65
|
+
case 'organizationName':
|
|
66
|
+
issueMap.organizationName = translate('onboarding.errors.organizationNameRequired', 'Organization name is required.')
|
|
67
|
+
break
|
|
68
|
+
case 'password':
|
|
69
|
+
issueMap.password = translate('onboarding.errors.passwordRequired', 'Password must be at least 6 characters.')
|
|
70
|
+
break
|
|
71
|
+
case 'confirmPassword':
|
|
72
|
+
issueMap.confirmPassword = translate('onboarding.errors.passwordMismatch', 'Passwords must match.')
|
|
73
|
+
break
|
|
74
|
+
case 'termsAccepted':
|
|
75
|
+
issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')
|
|
76
|
+
break
|
|
77
|
+
default:
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
if (!issueMap.termsAccepted && !termsAccepted) {
|
|
82
|
+
issueMap.termsAccepted = translate('onboarding.form.termsRequired', 'Please accept the terms to continue.')
|
|
83
|
+
}
|
|
84
|
+
setFieldErrors(issueMap)
|
|
85
|
+
setState('idle')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const call = await apiCall<{ ok?: boolean; error?: string; email?: string; fieldErrors?: Record<string, string> }>(
|
|
91
|
+
'/api/onboarding/onboarding',
|
|
92
|
+
{
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
body: JSON.stringify({ ...parsed.data, termsAccepted: true }),
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
const data = call.result ?? {}
|
|
99
|
+
if (!call.ok || data.ok === false) {
|
|
100
|
+
if (data.fieldErrors && typeof data.fieldErrors === 'object') {
|
|
101
|
+
const mapped: FieldErrors = {}
|
|
102
|
+
for (const key of Object.keys(data.fieldErrors)) {
|
|
103
|
+
const value = data.fieldErrors[key]
|
|
104
|
+
if (typeof value === 'string' && value.trim()) {
|
|
105
|
+
mapped[key as keyof FieldErrors] = value
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
setFieldErrors(mapped)
|
|
109
|
+
}
|
|
110
|
+
const message = typeof data.error === 'string' && data.error.trim()
|
|
111
|
+
? data.error
|
|
112
|
+
: translate('onboarding.form.genericError', 'Something went wrong. Please try again.')
|
|
113
|
+
setGlobalError(message)
|
|
114
|
+
setState('idle')
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
setEmailSubmitted(data.email ?? parsed.data.email)
|
|
118
|
+
setState('success')
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : ''
|
|
121
|
+
setGlobalError(message || translate('onboarding.form.genericError', 'Something went wrong. Please try again.'))
|
|
122
|
+
setState('idle')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const submitting = state === 'loading'
|
|
127
|
+
const disabled = submitting || state === 'success'
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="relative min-h-svh flex items-center justify-center bg-muted/40 px-4 pb-24">
|
|
131
|
+
<Card className="w-full max-w-lg shadow-lg">
|
|
132
|
+
<CardHeader className="flex flex-col gap-4 p-10 text-center">
|
|
133
|
+
<div className="flex flex-col items-center gap-3">
|
|
134
|
+
<Image alt="Open Mercato" src="/open-mercato.svg" width={120} height={120} priority />
|
|
135
|
+
<CardTitle className="text-2xl font-semibold">
|
|
136
|
+
{translate('onboarding.title', 'Create your Open Mercato workspace')}
|
|
137
|
+
</CardTitle>
|
|
138
|
+
<CardDescription>
|
|
139
|
+
{translate('onboarding.subtitle', 'Tell us a bit about you and we will set everything up.')}
|
|
140
|
+
</CardDescription>
|
|
141
|
+
</div>
|
|
142
|
+
</CardHeader>
|
|
143
|
+
<CardContent className="pb-10">
|
|
144
|
+
{state === 'success' && emailSubmitted && (
|
|
145
|
+
<div className="mb-6 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900" role="status" aria-live="polite">
|
|
146
|
+
<strong className="block text-sm font-medium">
|
|
147
|
+
{translate('onboarding.form.successTitle', 'Check your inbox')}
|
|
148
|
+
</strong>
|
|
149
|
+
<p>
|
|
150
|
+
{translate('onboarding.form.successBody', 'We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.', { email: emailSubmitted })}
|
|
151
|
+
</p>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
{state !== 'success' && globalError && (
|
|
155
|
+
<div className="mb-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" role="alert" aria-live="assertive">
|
|
156
|
+
{globalError}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
<form className="grid gap-4" onSubmit={onSubmit} noValidate>
|
|
160
|
+
<div className="grid gap-1">
|
|
161
|
+
<Label htmlFor="email">{translate('onboarding.form.email', 'Work email')}</Label>
|
|
162
|
+
<Input
|
|
163
|
+
id="email"
|
|
164
|
+
name="email"
|
|
165
|
+
type="email"
|
|
166
|
+
required
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
autoComplete="email"
|
|
169
|
+
aria-invalid={Boolean(fieldErrors.email)}
|
|
170
|
+
aria-describedby={fieldErrors.email ? 'email-error' : undefined}
|
|
171
|
+
className={fieldErrors.email ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
172
|
+
/>
|
|
173
|
+
{fieldErrors.email && (
|
|
174
|
+
<p id="email-error" className="text-xs text-red-600">{fieldErrors.email}</p>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
<div className="grid gap-1 sm:grid-cols-2 sm:gap-4">
|
|
178
|
+
<div className="grid gap-1">
|
|
179
|
+
<Label htmlFor="firstName">{translate('onboarding.form.firstName', 'First name')}</Label>
|
|
180
|
+
<Input
|
|
181
|
+
id="firstName"
|
|
182
|
+
name="firstName"
|
|
183
|
+
type="text"
|
|
184
|
+
required
|
|
185
|
+
disabled={disabled}
|
|
186
|
+
autoComplete="given-name"
|
|
187
|
+
aria-invalid={Boolean(fieldErrors.firstName)}
|
|
188
|
+
aria-describedby={fieldErrors.firstName ? 'firstName-error' : undefined}
|
|
189
|
+
className={fieldErrors.firstName ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
190
|
+
/>
|
|
191
|
+
{fieldErrors.firstName && (
|
|
192
|
+
<p id="firstName-error" className="text-xs text-red-600">{fieldErrors.firstName}</p>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="grid gap-1">
|
|
196
|
+
<Label htmlFor="lastName">{translate('onboarding.form.lastName', 'Last name')}</Label>
|
|
197
|
+
<Input
|
|
198
|
+
id="lastName"
|
|
199
|
+
name="lastName"
|
|
200
|
+
type="text"
|
|
201
|
+
required
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
autoComplete="family-name"
|
|
204
|
+
aria-invalid={Boolean(fieldErrors.lastName)}
|
|
205
|
+
aria-describedby={fieldErrors.lastName ? 'lastName-error' : undefined}
|
|
206
|
+
className={fieldErrors.lastName ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
207
|
+
/>
|
|
208
|
+
{fieldErrors.lastName && (
|
|
209
|
+
<p id="lastName-error" className="text-xs text-red-600">{fieldErrors.lastName}</p>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div className="grid gap-1">
|
|
214
|
+
<Label htmlFor="organizationName">{translate('onboarding.form.organizationName', 'Organization name')}</Label>
|
|
215
|
+
<Input
|
|
216
|
+
id="organizationName"
|
|
217
|
+
name="organizationName"
|
|
218
|
+
type="text"
|
|
219
|
+
required
|
|
220
|
+
disabled={disabled}
|
|
221
|
+
autoComplete="organization"
|
|
222
|
+
aria-invalid={Boolean(fieldErrors.organizationName)}
|
|
223
|
+
aria-describedby={fieldErrors.organizationName ? 'organizationName-error' : undefined}
|
|
224
|
+
className={fieldErrors.organizationName ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
225
|
+
/>
|
|
226
|
+
{fieldErrors.organizationName && (
|
|
227
|
+
<p id="organizationName-error" className="text-xs text-red-600">{fieldErrors.organizationName}</p>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<div className="grid gap-1">
|
|
231
|
+
<Label htmlFor="password">{translate('onboarding.form.password', 'Password')}</Label>
|
|
232
|
+
<Input
|
|
233
|
+
id="password"
|
|
234
|
+
name="password"
|
|
235
|
+
type="password"
|
|
236
|
+
required
|
|
237
|
+
disabled={disabled}
|
|
238
|
+
autoComplete="new-password"
|
|
239
|
+
aria-invalid={Boolean(fieldErrors.password)}
|
|
240
|
+
aria-describedby={fieldErrors.password ? 'password-error' : undefined}
|
|
241
|
+
className={fieldErrors.password ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
242
|
+
/>
|
|
243
|
+
{fieldErrors.password && (
|
|
244
|
+
<p id="password-error" className="text-xs text-red-600">{fieldErrors.password}</p>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
<div className="grid gap-1">
|
|
248
|
+
<Label htmlFor="confirmPassword">{translate('onboarding.form.confirmPassword', 'Confirm password')}</Label>
|
|
249
|
+
<Input
|
|
250
|
+
id="confirmPassword"
|
|
251
|
+
name="confirmPassword"
|
|
252
|
+
type="password"
|
|
253
|
+
required
|
|
254
|
+
disabled={disabled}
|
|
255
|
+
autoComplete="new-password"
|
|
256
|
+
aria-invalid={Boolean(fieldErrors.confirmPassword)}
|
|
257
|
+
aria-describedby={fieldErrors.confirmPassword ? 'confirmPassword-error' : undefined}
|
|
258
|
+
className={fieldErrors.confirmPassword ? 'border-red-500 focus-visible:ring-red-500' : undefined}
|
|
259
|
+
/>
|
|
260
|
+
{fieldErrors.confirmPassword && (
|
|
261
|
+
<p id="confirmPassword-error" className="text-xs text-red-600">{fieldErrors.confirmPassword}</p>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
<label className="flex items-start gap-3 text-sm text-muted-foreground">
|
|
265
|
+
<Checkbox
|
|
266
|
+
id="terms"
|
|
267
|
+
checked={termsAccepted}
|
|
268
|
+
disabled={disabled}
|
|
269
|
+
onCheckedChange={(value: boolean | 'indeterminate') => {
|
|
270
|
+
setTermsAccepted(value === true)
|
|
271
|
+
if (value === true) {
|
|
272
|
+
setFieldErrors((prev) => {
|
|
273
|
+
const next = { ...prev }
|
|
274
|
+
delete next.termsAccepted
|
|
275
|
+
return next
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}}
|
|
279
|
+
aria-invalid={Boolean(fieldErrors.termsAccepted)}
|
|
280
|
+
/>
|
|
281
|
+
<span>
|
|
282
|
+
{translate('onboarding.form.termsLabel', 'I have read and accept the terms of service')}{' '}
|
|
283
|
+
<a className="underline hover:text-foreground" href="/terms" target="_blank" rel="noreferrer">
|
|
284
|
+
{translate('onboarding.form.termsLink', 'terms of service')}
|
|
285
|
+
</a>
|
|
286
|
+
{fieldErrors.termsAccepted && (
|
|
287
|
+
<span className="mt-1 block text-xs text-red-600">{fieldErrors.termsAccepted}</span>
|
|
288
|
+
)}
|
|
289
|
+
</span>
|
|
290
|
+
</label>
|
|
291
|
+
<button
|
|
292
|
+
type="submit"
|
|
293
|
+
disabled={disabled}
|
|
294
|
+
className="mt-2 h-11 rounded-md bg-foreground text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
|
295
|
+
>
|
|
296
|
+
{submitting
|
|
297
|
+
? translate('onboarding.form.loading', 'Sending...')
|
|
298
|
+
: translate('onboarding.form.submit', 'Send verification email')}
|
|
299
|
+
</button>
|
|
300
|
+
</form>
|
|
301
|
+
</CardContent>
|
|
302
|
+
</Card>
|
|
303
|
+
</div>
|
|
304
|
+
)
|
|
305
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"onboarding": {
|
|
3
|
+
"title": "Create your Open Mercato workspace",
|
|
4
|
+
"subtitle": "Tell us a bit about you and we will set everything up.",
|
|
5
|
+
"form": {
|
|
6
|
+
"email": "Work email",
|
|
7
|
+
"firstName": "First name",
|
|
8
|
+
"lastName": "Last name",
|
|
9
|
+
"organizationName": "Organization name",
|
|
10
|
+
"password": "Password",
|
|
11
|
+
"confirmPassword": "Confirm password",
|
|
12
|
+
"termsLabel": "I have read and accept the terms of service",
|
|
13
|
+
"submit": "Send verification email",
|
|
14
|
+
"loading": "Sending...",
|
|
15
|
+
"successTitle": "Check your inbox",
|
|
16
|
+
"successBody": "We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.",
|
|
17
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
18
|
+
"genericError": "Something went wrong. Please try again or contact support.",
|
|
19
|
+
"termsLink": "terms of service",
|
|
20
|
+
"termsRequired": "Please accept the terms to continue."
|
|
21
|
+
},
|
|
22
|
+
"email": {
|
|
23
|
+
"subject": "Confirm your email to finish onboarding",
|
|
24
|
+
"preview": "Confirm your email to activate your Open Mercato workspace",
|
|
25
|
+
"heading": "Welcome to Open Mercato",
|
|
26
|
+
"greeting": "Hi {firstName},",
|
|
27
|
+
"body": "We just need to confirm your email address to finish setting up the organization {organizationName}.",
|
|
28
|
+
"cta": "Confirm email & activate workspace",
|
|
29
|
+
"expiry": "The link will expire in 24 hours. If you didn't request this, you can safely ignore this message.",
|
|
30
|
+
"footer": "Open Mercato · Tenant onboarding service",
|
|
31
|
+
"adminSubject": "New self-service onboarding request",
|
|
32
|
+
"adminPreview": "New onboarding request submitted",
|
|
33
|
+
"adminHeading": "New onboarding request",
|
|
34
|
+
"adminBody": "{firstName} {lastName} ({email}) submitted an onboarding request for {organizationName}.",
|
|
35
|
+
"adminFooter": "You can review the tenant after verification is complete."
|
|
36
|
+
},
|
|
37
|
+
"errors": {
|
|
38
|
+
"emailInvalid": "Enter a valid work email.",
|
|
39
|
+
"firstNameRequired": "First name is required.",
|
|
40
|
+
"lastNameRequired": "Last name is required.",
|
|
41
|
+
"organizationNameRequired": "Organization name is required.",
|
|
42
|
+
"passwordRequired": "Password must be at least 6 characters.",
|
|
43
|
+
"passwordMismatch": "Passwords must match.",
|
|
44
|
+
"termsRequired": "Please accept the terms to continue.",
|
|
45
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
46
|
+
"pendingRequest": "We already have a pending verification. Please try again in about {minutes} minutes or contact the administrator."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"onboarding": {
|
|
3
|
+
"title": "Create your Open Mercato workspace",
|
|
4
|
+
"subtitle": "Tell us a bit about you and we will set everything up.",
|
|
5
|
+
"form": {
|
|
6
|
+
"email": "Work email",
|
|
7
|
+
"firstName": "First name",
|
|
8
|
+
"lastName": "Last name",
|
|
9
|
+
"organizationName": "Organization name",
|
|
10
|
+
"password": "Password",
|
|
11
|
+
"confirmPassword": "Confirm password",
|
|
12
|
+
"termsLabel": "I have read and accept the terms of service",
|
|
13
|
+
"submit": "Send verification email",
|
|
14
|
+
"loading": "Sending...",
|
|
15
|
+
"successTitle": "Check your inbox",
|
|
16
|
+
"successBody": "We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.",
|
|
17
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
18
|
+
"genericError": "Something went wrong. Please try again or contact support.",
|
|
19
|
+
"termsLink": "terms of service",
|
|
20
|
+
"termsRequired": "Please accept the terms to continue."
|
|
21
|
+
},
|
|
22
|
+
"email": {
|
|
23
|
+
"subject": "Confirm your email to finish onboarding",
|
|
24
|
+
"preview": "Confirm your email to activate your Open Mercato workspace",
|
|
25
|
+
"heading": "Welcome to Open Mercato",
|
|
26
|
+
"greeting": "Hi {firstName},",
|
|
27
|
+
"body": "We just need to confirm your email address to finish setting up the organization {organizationName}.",
|
|
28
|
+
"cta": "Confirm email & activate workspace",
|
|
29
|
+
"expiry": "The link will expire in 24 hours. If you didn't request this, you can safely ignore this message.",
|
|
30
|
+
"footer": "Open Mercato · Tenant onboarding service",
|
|
31
|
+
"adminSubject": "New self-service onboarding request",
|
|
32
|
+
"adminPreview": "New onboarding request submitted",
|
|
33
|
+
"adminHeading": "New onboarding request",
|
|
34
|
+
"adminBody": "{firstName} {lastName} ({email}) submitted an onboarding request for {organizationName}.",
|
|
35
|
+
"adminFooter": "You can review the tenant after verification is complete."
|
|
36
|
+
},
|
|
37
|
+
"errors": {
|
|
38
|
+
"emailInvalid": "Enter a valid work email.",
|
|
39
|
+
"firstNameRequired": "First name is required.",
|
|
40
|
+
"lastNameRequired": "Last name is required.",
|
|
41
|
+
"organizationNameRequired": "Organization name is required.",
|
|
42
|
+
"passwordRequired": "Password must be at least 6 characters.",
|
|
43
|
+
"passwordMismatch": "Passwords must match.",
|
|
44
|
+
"termsRequired": "Please accept the terms to continue.",
|
|
45
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
46
|
+
"pendingRequest": "We already have a pending verification. Please try again in about {minutes} minutes or contact the administrator."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"onboarding": {
|
|
3
|
+
"title": "Create your Open Mercato workspace",
|
|
4
|
+
"subtitle": "Tell us a bit about you and we will set everything up.",
|
|
5
|
+
"form": {
|
|
6
|
+
"email": "Work email",
|
|
7
|
+
"firstName": "First name",
|
|
8
|
+
"lastName": "Last name",
|
|
9
|
+
"organizationName": "Organization name",
|
|
10
|
+
"password": "Password",
|
|
11
|
+
"confirmPassword": "Confirm password",
|
|
12
|
+
"termsLabel": "I have read and accept the terms of service",
|
|
13
|
+
"submit": "Send verification email",
|
|
14
|
+
"loading": "Sending...",
|
|
15
|
+
"successTitle": "Check your inbox",
|
|
16
|
+
"successBody": "We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.",
|
|
17
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
18
|
+
"genericError": "Something went wrong. Please try again or contact support.",
|
|
19
|
+
"termsLink": "terms of service",
|
|
20
|
+
"termsRequired": "Please accept the terms to continue."
|
|
21
|
+
},
|
|
22
|
+
"email": {
|
|
23
|
+
"subject": "Confirm your email to finish onboarding",
|
|
24
|
+
"preview": "Confirm your email to activate your Open Mercato workspace",
|
|
25
|
+
"heading": "Welcome to Open Mercato",
|
|
26
|
+
"greeting": "Hi {firstName},",
|
|
27
|
+
"body": "We just need to confirm your email address to finish setting up the organization {organizationName}.",
|
|
28
|
+
"cta": "Confirm email & activate workspace",
|
|
29
|
+
"expiry": "The link will expire in 24 hours. If you didn't request this, you can safely ignore this message.",
|
|
30
|
+
"footer": "Open Mercato · Tenant onboarding service",
|
|
31
|
+
"adminSubject": "New self-service onboarding request",
|
|
32
|
+
"adminPreview": "New onboarding request submitted",
|
|
33
|
+
"adminHeading": "New onboarding request",
|
|
34
|
+
"adminBody": "{firstName} {lastName} ({email}) submitted an onboarding request for {organizationName}.",
|
|
35
|
+
"adminFooter": "You can review the tenant after verification is complete."
|
|
36
|
+
},
|
|
37
|
+
"errors": {
|
|
38
|
+
"emailInvalid": "Enter a valid work email.",
|
|
39
|
+
"firstNameRequired": "First name is required.",
|
|
40
|
+
"lastNameRequired": "Last name is required.",
|
|
41
|
+
"organizationNameRequired": "Organization name is required.",
|
|
42
|
+
"passwordRequired": "Password must be at least 6 characters.",
|
|
43
|
+
"passwordMismatch": "Passwords must match.",
|
|
44
|
+
"termsRequired": "Please accept the terms to continue.",
|
|
45
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
46
|
+
"pendingRequest": "We already have a pending verification. Please try again in about {minutes} minutes or contact the administrator."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"onboarding": {
|
|
3
|
+
"title": "Create your Open Mercato workspace",
|
|
4
|
+
"subtitle": "Tell us a bit about you and we will set everything up.",
|
|
5
|
+
"form": {
|
|
6
|
+
"email": "Work email",
|
|
7
|
+
"firstName": "First name",
|
|
8
|
+
"lastName": "Last name",
|
|
9
|
+
"organizationName": "Organization name",
|
|
10
|
+
"password": "Password",
|
|
11
|
+
"confirmPassword": "Confirm password",
|
|
12
|
+
"termsLabel": "I have read and accept the terms of service",
|
|
13
|
+
"submit": "Send verification email",
|
|
14
|
+
"loading": "Sending...",
|
|
15
|
+
"successTitle": "Check your inbox",
|
|
16
|
+
"successBody": "We sent a verification link to {email}. Confirm it within 24 hours to activate your workspace.",
|
|
17
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
18
|
+
"genericError": "Something went wrong. Please try again or contact support.",
|
|
19
|
+
"termsLink": "terms of service",
|
|
20
|
+
"termsRequired": "Please accept the terms to continue."
|
|
21
|
+
},
|
|
22
|
+
"email": {
|
|
23
|
+
"subject": "Confirm your email to finish onboarding",
|
|
24
|
+
"preview": "Confirm your email to activate your Open Mercato workspace",
|
|
25
|
+
"heading": "Welcome to Open Mercato",
|
|
26
|
+
"greeting": "Hi {firstName},",
|
|
27
|
+
"body": "We just need to confirm your email address to finish setting up the organization {organizationName}.",
|
|
28
|
+
"cta": "Confirm email & activate workspace",
|
|
29
|
+
"expiry": "The link will expire in 24 hours. If you didn't request this, you can safely ignore this message.",
|
|
30
|
+
"footer": "Open Mercato · Tenant onboarding service",
|
|
31
|
+
"adminSubject": "New self-service onboarding request",
|
|
32
|
+
"adminPreview": "New onboarding request submitted",
|
|
33
|
+
"adminHeading": "New onboarding request",
|
|
34
|
+
"adminBody": "{firstName} {lastName} ({email}) submitted an onboarding request for {organizationName}.",
|
|
35
|
+
"adminFooter": "You can review the tenant after verification is complete."
|
|
36
|
+
},
|
|
37
|
+
"errors": {
|
|
38
|
+
"emailInvalid": "Enter a valid work email.",
|
|
39
|
+
"firstNameRequired": "First name is required.",
|
|
40
|
+
"lastNameRequired": "Last name is required.",
|
|
41
|
+
"organizationNameRequired": "Organization name is required.",
|
|
42
|
+
"passwordRequired": "Password must be at least 6 characters.",
|
|
43
|
+
"passwordMismatch": "Passwords must match.",
|
|
44
|
+
"termsRequired": "Please accept the terms to continue.",
|
|
45
|
+
"emailExists": "We already have an account with this email. Try signing in or resetting your password.",
|
|
46
|
+
"pendingRequest": "We already have a pending verification. Please try again in about {minutes} minutes or contact the administrator."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleInfo } from '@open-mercato/shared/modules/registry'
|
|
2
|
+
|
|
3
|
+
export const metadata: ModuleInfo = {
|
|
4
|
+
name: 'onboarding',
|
|
5
|
+
title: 'Onboarding',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
description: 'Self-service tenant and organization onboarding flow.',
|
|
8
|
+
author: 'Open Mercato Team',
|
|
9
|
+
license: 'Proprietary',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { features } from './acl'
|