@open-mercato/core 0.6.1-develop.3276.1.1fcd145078 → 0.6.1-develop.3278.1.a4d26475e4
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/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/domain_mapping/index.js +31 -0
- package/dist/generated/entities/domain_mapping/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +16 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +18 -17
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/customer_accounts/acl.js +2 -1
- package/dist/modules/customer_accounts/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js +110 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js +114 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.js.map +7 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +271 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-check.js +70 -0
- package/dist/modules/customer_accounts/api/domain-check.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js +128 -0
- package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +7 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js +84 -0
- package/dist/modules/customer_accounts/api/domain-resolve.js.map +7 -0
- package/dist/modules/customer_accounts/api/login.js +14 -3
- package/dist/modules/customer_accounts/api/login.js.map +2 -2
- package/dist/modules/customer_accounts/api/magic-link/request.js +14 -3
- package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
- package/dist/modules/customer_accounts/api/password/reset-request.js +14 -3
- package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +32 -5
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js +48 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +43 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js +53 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js +69 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js +31 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js +147 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js +39 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js +60 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js +11 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js +1 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js +361 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.js.map +7 -0
- package/dist/modules/customer_accounts/data/enrichers.js +78 -12
- package/dist/modules/customer_accounts/data/enrichers.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +85 -2
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/guards.js +130 -0
- package/dist/modules/customer_accounts/data/guards.js.map +7 -0
- package/dist/modules/customer_accounts/data/validators.js +19 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +23 -1
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +9 -2
- package/dist/modules/customer_accounts/events.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +27 -7
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +48 -0
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +7 -0
- package/dist/modules/customer_accounts/lib/hostname.js +55 -0
- package/dist/modules/customer_accounts/lib/hostname.js.map +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js +7 -0
- package/dist/modules/customer_accounts/lib/platformDomains.js.map +7 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js +71 -0
- package/dist/modules/customer_accounts/lib/proxyRanges.js.map +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js +7 -0
- package/dist/modules/customer_accounts/lib/rateLimiter.js.map +2 -2
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js +63 -0
- package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +7 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js +13 -0
- package/dist/modules/customer_accounts/lib/secretCompare.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js +37 -0
- package/dist/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/notifications.client.js +76 -0
- package/dist/modules/customer_accounts/notifications.client.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +76 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +497 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +48 -1
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js +29 -0
- package/dist/modules/customer_accounts/subscribers/invalidateDomainCache.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainActivated.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js +30 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainDnsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js +31 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainTlsFailed.js.map +7 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js +29 -0
- package/dist/modules/customer_accounts/subscribers/notifyDomainVerified.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js +67 -0
- package/dist/modules/customer_accounts/workers/domainTlsRetryWorker.js.map +7 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js +34 -0
- package/dist/modules/customer_accounts/workers/domainVerificationWorker.js.map +7 -0
- package/dist/modules/customers/api/companies/route.js +4 -9
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +4 -9
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +4 -9
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +0 -12
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +361 -284
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +378 -473
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +368 -286
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +15 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +188 -36
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +2 -2
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +2 -2
- package/dist/modules/customers/widgets/injection-table.js +6 -0
- package/dist/modules/customers/widgets/injection-table.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +16 -2
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +1 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +31 -8
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/generated/entities/domain_mapping/index.ts +14 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +16 -0
- package/package.json +5 -5
- package/src/helpers/integration/salesUi.ts +27 -23
- package/src/modules/customer_accounts/acl.ts +1 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/health-check.ts +119 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings/[id]/verify.ts +123 -0
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +303 -0
- package/src/modules/customer_accounts/api/domain-check.ts +83 -0
- package/src/modules/customer_accounts/api/domain-resolve/all.ts +139 -0
- package/src/modules/customer_accounts/api/domain-resolve.ts +91 -0
- package/src/modules/customer_accounts/api/login.ts +14 -3
- package/src/modules/customer_accounts/api/magic-link/request.ts +14 -3
- package/src/modules/customer_accounts/api/password/reset-request.ts +15 -3
- package/src/modules/customer_accounts/api/signup.ts +36 -5
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Actions.tsx +70 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +66 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DnsConfig.tsx +78 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/DomainPanel.tsx +90 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/OrgSwitcher.tsx +43 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/RegisterDialog.tsx +178 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/StatusBadgeSection.tsx +56 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Stepper.tsx +69 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/domainStatusMap.ts +10 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/types.ts +31 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.meta.ts +26 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/page.tsx +411 -0
- package/src/modules/customer_accounts/data/enrichers.ts +95 -12
- package/src/modules/customer_accounts/data/entities.ts +80 -1
- package/src/modules/customer_accounts/data/guards.ts +155 -0
- package/src/modules/customer_accounts/data/validators.ts +26 -0
- package/src/modules/customer_accounts/di.ts +35 -1
- package/src/modules/customer_accounts/events.ts +8 -1
- package/src/modules/customer_accounts/i18n/de.json +78 -0
- package/src/modules/customer_accounts/i18n/en.json +78 -0
- package/src/modules/customer_accounts/i18n/es.json +78 -0
- package/src/modules/customer_accounts/i18n/pl.json +78 -0
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +44 -8
- package/src/modules/customer_accounts/lib/customerUrl.ts +72 -0
- package/src/modules/customer_accounts/lib/hostname.ts +76 -0
- package/src/modules/customer_accounts/lib/platformDomains.ts +14 -0
- package/src/modules/customer_accounts/lib/proxyRanges.ts +85 -0
- package/src/modules/customer_accounts/lib/rateLimiter.ts +10 -0
- package/src/modules/customer_accounts/lib/resolveTenantContext.ts +96 -0
- package/src/modules/customer_accounts/lib/secretCompare.ts +20 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +341 -0
- package/src/modules/customer_accounts/migrations/Migration20260430120000_customer_accounts.ts +38 -0
- package/src/modules/customer_accounts/notifications.client.ts +76 -0
- package/src/modules/customer_accounts/notifications.ts +76 -0
- package/src/modules/customer_accounts/services/domainMappingService.ts +632 -0
- package/src/modules/customer_accounts/setup.ts +81 -1
- package/src/modules/customer_accounts/subscribers/invalidateDomainCache.ts +35 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainActivated.ts +32 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainDnsFailed.ts +33 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainTlsFailed.ts +34 -0
- package/src/modules/customer_accounts/subscribers/notifyDomainVerified.ts +32 -0
- package/src/modules/customer_accounts/workers/domainTlsRetryWorker.ts +84 -0
- package/src/modules/customer_accounts/workers/domainVerificationWorker.ts +45 -0
- package/src/modules/customers/api/companies/route.ts +4 -9
- package/src/modules/customers/api/deals/route.ts +4 -9
- package/src/modules/customers/api/people/route.ts +4 -9
- package/src/modules/customers/api/utils.ts +0 -14
- package/src/modules/customers/backend/customers/companies/page.tsx +321 -237
- package/src/modules/customers/backend/customers/deals/page.tsx +321 -455
- package/src/modules/customers/backend/customers/people/page.tsx +331 -240
- package/src/modules/customers/components/detail/assignableStaff.ts +21 -0
- package/src/modules/customers/i18n/de.json +31 -0
- package/src/modules/customers/i18n/en.json +31 -0
- package/src/modules/customers/i18n/es.json +31 -0
- package/src/modules/customers/i18n/pl.json +31 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +197 -40
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +13 -7
- package/src/modules/customers/widgets/injection-table.ts +6 -0
- package/src/modules/perspectives/services/perspectiveService.ts +30 -2
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +1 -0
- package/src/modules/query_index/lib/engine.ts +46 -8
|
@@ -2,6 +2,10 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
4
|
import { passwordResetRequestSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
5
|
+
import {
|
|
6
|
+
resolveTenantContext,
|
|
7
|
+
TenantResolutionError,
|
|
8
|
+
} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
|
|
5
9
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
10
|
import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
|
|
7
11
|
import { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'
|
|
@@ -37,9 +41,17 @@ export async function POST(req: Request) {
|
|
|
37
41
|
return NextResponse.json({ ok: true }) // Always 200 to prevent enumeration
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
const { email
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const { email } = parsed.data
|
|
45
|
+
let tenantId: string
|
|
46
|
+
try {
|
|
47
|
+
const context = await resolveTenantContext(req, parsed.data.tenantId)
|
|
48
|
+
tenantId = context.tenantId
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err instanceof TenantResolutionError) {
|
|
51
|
+
// Always return 200 to avoid leaking whether a tenant exists for this host.
|
|
52
|
+
return NextResponse.json({ ok: true })
|
|
53
|
+
}
|
|
54
|
+
throw err
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
const container = await createRequestContainer()
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
import { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'
|
|
22
22
|
import { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'
|
|
23
23
|
import { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'
|
|
24
|
+
import {
|
|
25
|
+
resolveTenantContext,
|
|
26
|
+
TenantResolutionError,
|
|
27
|
+
} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'
|
|
28
|
+
import { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'
|
|
24
29
|
|
|
25
30
|
export const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }
|
|
26
31
|
|
|
@@ -63,9 +68,21 @@ export async function POST(req: Request) {
|
|
|
63
68
|
return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
const { email, password, displayName
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const { email, password, displayName } = parsed.data
|
|
72
|
+
let tenantId: string
|
|
73
|
+
let organizationId: string | null
|
|
74
|
+
try {
|
|
75
|
+
const context = await resolveTenantContext(req, parsed.data.tenantId)
|
|
76
|
+
tenantId = context.tenantId
|
|
77
|
+
organizationId = context.organizationId ?? parsed.data.organizationId ?? null
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof TenantResolutionError) {
|
|
80
|
+
return NextResponse.json({ ok: false, error: err.message }, { status: err.status })
|
|
81
|
+
}
|
|
82
|
+
throw err
|
|
83
|
+
}
|
|
84
|
+
if (!organizationId) {
|
|
85
|
+
return NextResponse.json({ ok: false, error: 'organizationId is required' }, { status: 400 })
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
let baseUrl: string
|
|
@@ -95,7 +112,14 @@ export async function POST(req: Request) {
|
|
|
95
112
|
if (existing) {
|
|
96
113
|
await bcryptCompare(password, TIMING_EQUALIZATION_HASH)
|
|
97
114
|
const existingOrg = await findOrganizationInTenant(em, existing.organizationId, tenantId)
|
|
98
|
-
|
|
115
|
+
// Prefer the org's active custom domain when available; fall back to the
|
|
116
|
+
// platform login URL preserved by `resolvePortalLoginUrl`.
|
|
117
|
+
let loginUrl = resolvePortalLoginUrl(baseUrl, existingOrg?.slug ?? null)
|
|
118
|
+
try {
|
|
119
|
+
loginUrl = await urlForCustomerOrg(existing.organizationId, '/login', { container })
|
|
120
|
+
} catch {
|
|
121
|
+
// Fall back to platform URL on any resolution issue.
|
|
122
|
+
}
|
|
99
123
|
const subject = translate('customer_accounts.signup.existing.subject', 'You already have a portal account')
|
|
100
124
|
const copy = {
|
|
101
125
|
preview: translate('customer_accounts.signup.existing.preview', 'A sign-up attempt was made for an email that already has a portal account.'),
|
|
@@ -141,7 +165,14 @@ export async function POST(req: Request) {
|
|
|
141
165
|
await em.persist(user).flush()
|
|
142
166
|
|
|
143
167
|
const verificationToken = await customerTokenService.createEmailVerification(user.id, tenantId)
|
|
144
|
-
|
|
168
|
+
let verifyUrl = resolvePortalVerifyUrl(baseUrl, verificationToken, orgRow.slug)
|
|
169
|
+
try {
|
|
170
|
+
verifyUrl = await urlForCustomerOrg(organizationId, `/verify?token=${encodeURIComponent(verificationToken)}`, {
|
|
171
|
+
container,
|
|
172
|
+
})
|
|
173
|
+
} catch {
|
|
174
|
+
// Fall back to the platform-derived URL on any resolution issue.
|
|
175
|
+
}
|
|
145
176
|
const subject = translate('customer_accounts.signup.created.subject', 'Verify your portal account')
|
|
146
177
|
const copy = {
|
|
147
178
|
preview: translate('customer_accounts.signup.created.preview', 'Verify your portal account to finish sign-up.'),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { ExternalLink, RefreshCw, Replace, Trash2 } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import type { DomainMappingRow } from './types'
|
|
8
|
+
|
|
9
|
+
export type ActionsProps = {
|
|
10
|
+
mapping: DomainMappingRow
|
|
11
|
+
isReplacement: boolean
|
|
12
|
+
onCheckNow: () => void
|
|
13
|
+
onRetryTls: () => void
|
|
14
|
+
onChangeDomain: () => void
|
|
15
|
+
onRemove: () => void
|
|
16
|
+
busy?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DefaultActions({
|
|
20
|
+
mapping,
|
|
21
|
+
isReplacement,
|
|
22
|
+
onCheckNow,
|
|
23
|
+
onRetryTls,
|
|
24
|
+
onChangeDomain,
|
|
25
|
+
onRemove,
|
|
26
|
+
busy,
|
|
27
|
+
}: ActionsProps) {
|
|
28
|
+
const t = useT()
|
|
29
|
+
const showCheckNow = mapping.status === 'pending' || mapping.status === 'dns_failed'
|
|
30
|
+
const showRetryTls = mapping.status === 'tls_failed' || mapping.status === 'verified'
|
|
31
|
+
const showTestDomain = mapping.status === 'active'
|
|
32
|
+
const showChangeDomain = mapping.status === 'active' && !isReplacement
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
36
|
+
{showCheckNow ? (
|
|
37
|
+
<Button type="button" variant="outline" size="sm" onClick={onCheckNow} disabled={busy}>
|
|
38
|
+
<RefreshCw className="mr-1.5 h-4 w-4" aria-hidden />
|
|
39
|
+
{t('customer_accounts.domainMapping.verifyNow', 'Check Now')}
|
|
40
|
+
</Button>
|
|
41
|
+
) : null}
|
|
42
|
+
{showRetryTls ? (
|
|
43
|
+
<Button type="button" variant="outline" size="sm" onClick={onRetryTls} disabled={busy}>
|
|
44
|
+
<RefreshCw className="mr-1.5 h-4 w-4" aria-hidden />
|
|
45
|
+
{t('customer_accounts.domainMapping.tls.retryButton', 'Retry SSL')}
|
|
46
|
+
</Button>
|
|
47
|
+
) : null}
|
|
48
|
+
{showTestDomain ? (
|
|
49
|
+
<Button type="button" variant="outline" size="sm" asChild>
|
|
50
|
+
<a href={`https://${mapping.hostname}`} target="_blank" rel="noopener noreferrer">
|
|
51
|
+
<ExternalLink className="mr-1.5 h-4 w-4" aria-hidden />
|
|
52
|
+
{t('customer_accounts.domainMapping.preview', 'Test Domain')}
|
|
53
|
+
</a>
|
|
54
|
+
</Button>
|
|
55
|
+
) : null}
|
|
56
|
+
{showChangeDomain ? (
|
|
57
|
+
<Button type="button" variant="outline" size="sm" onClick={onChangeDomain} disabled={busy}>
|
|
58
|
+
<Replace className="mr-1.5 h-4 w-4" aria-hidden />
|
|
59
|
+
{t('customer_accounts.domainMapping.changeDomain', 'Change Domain')}
|
|
60
|
+
</Button>
|
|
61
|
+
) : null}
|
|
62
|
+
<Button type="button" variant="outline" size="sm" onClick={onRemove} disabled={busy}>
|
|
63
|
+
<Trash2 className="mr-1.5 h-4 w-4" aria-hidden />
|
|
64
|
+
{t('customer_accounts.domainMapping.removeDomain', 'Remove Domain')}
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default DefaultActions
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { AlertTriangle } from 'lucide-react'
|
|
5
|
+
import { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import type { DomainMappingRow } from './types'
|
|
8
|
+
|
|
9
|
+
export type DiagnosticsProps = {
|
|
10
|
+
mapping: DomainMappingRow
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DnsDiagnostics({ mapping }: DiagnosticsProps) {
|
|
14
|
+
const t = useT()
|
|
15
|
+
if (mapping.status !== 'dns_failed') return null
|
|
16
|
+
return (
|
|
17
|
+
<Alert variant="destructive">
|
|
18
|
+
<AlertTriangle className="h-4 w-4" aria-hidden />
|
|
19
|
+
<AlertTitle>
|
|
20
|
+
{t('customer_accounts.domainMapping.dns.diagnostics.title', 'DNS configuration issue')}
|
|
21
|
+
</AlertTitle>
|
|
22
|
+
<AlertDescription>
|
|
23
|
+
<div className="space-y-2">
|
|
24
|
+
{mapping.dnsFailureReason ? <p>{mapping.dnsFailureReason}</p> : null}
|
|
25
|
+
{mapping.cnameTarget ? (
|
|
26
|
+
<dl className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-xs">
|
|
27
|
+
<dt className="text-muted-foreground">
|
|
28
|
+
{t('customer_accounts.domainMapping.dns.diagnostics.expected', 'Expected target')}
|
|
29
|
+
</dt>
|
|
30
|
+
<dd className="font-mono">{mapping.cnameTarget}</dd>
|
|
31
|
+
</dl>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
</AlertDescription>
|
|
35
|
+
</Alert>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TlsDiagnostics({ mapping }: DiagnosticsProps) {
|
|
40
|
+
const t = useT()
|
|
41
|
+
if (mapping.status !== 'tls_failed') return null
|
|
42
|
+
return (
|
|
43
|
+
<Alert variant="warning">
|
|
44
|
+
<AlertTriangle className="h-4 w-4" aria-hidden />
|
|
45
|
+
<AlertTitle>
|
|
46
|
+
{t('customer_accounts.domainMapping.tls.diagnostics.title', 'SSL certificate issue')}
|
|
47
|
+
</AlertTitle>
|
|
48
|
+
<AlertDescription>
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
{mapping.tlsFailureReason ? <p>{mapping.tlsFailureReason}</p> : null}
|
|
51
|
+
<p className="text-xs text-muted-foreground">
|
|
52
|
+
{t('customer_accounts.domainMapping.tls.diagnostics.retryCount', 'Retry attempts: {count}', {
|
|
53
|
+
count: String(mapping.tlsRetryCount),
|
|
54
|
+
})}
|
|
55
|
+
</p>
|
|
56
|
+
<p className="text-xs text-muted-foreground">
|
|
57
|
+
{t(
|
|
58
|
+
'customer_accounts.domainMapping.tls.diagnostics.operatorNote',
|
|
59
|
+
'We are retrying automatically. If this persists, contact platform support — your DNS is fine, this is on our side.',
|
|
60
|
+
)}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</AlertDescription>
|
|
64
|
+
</Alert>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Copy, Check } from 'lucide-react'
|
|
5
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
|
|
8
|
+
export type DnsConfigProps = {
|
|
9
|
+
hostname: string | null
|
|
10
|
+
cnameTarget: string | null
|
|
11
|
+
aRecordTarget: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function CopyButton({ value, label }: { value: string; label: string }) {
|
|
15
|
+
const t = useT()
|
|
16
|
+
const [copied, setCopied] = React.useState(false)
|
|
17
|
+
|
|
18
|
+
const handleCopy = React.useCallback(async () => {
|
|
19
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) return
|
|
20
|
+
await navigator.clipboard.writeText(value)
|
|
21
|
+
setCopied(true)
|
|
22
|
+
window.setTimeout(() => setCopied(false), 1500)
|
|
23
|
+
}, [value])
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Button type="button" variant="ghost" size="sm" onClick={handleCopy} aria-label={label}>
|
|
27
|
+
{copied ? <Check className="h-4 w-4" aria-hidden /> : <Copy className="h-4 w-4" aria-hidden />}
|
|
28
|
+
<span className="ml-1.5 text-xs">
|
|
29
|
+
{copied
|
|
30
|
+
? t('customer_accounts.domainMapping.copyTarget.copied', 'Copied')
|
|
31
|
+
: t('customer_accounts.domainMapping.copyTarget', 'Copy')}
|
|
32
|
+
</span>
|
|
33
|
+
</Button>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DefaultDnsConfig({ hostname, cnameTarget, aRecordTarget }: DnsConfigProps) {
|
|
38
|
+
const t = useT()
|
|
39
|
+
const exampleHost = hostname ?? t('customer_accounts.domainMapping.hostname.placeholder', 'e.g., shop.yourdomain.com')
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-4 rounded-lg border bg-muted/20 p-4">
|
|
43
|
+
<div className="space-y-2">
|
|
44
|
+
<p className="text-sm font-medium text-foreground">
|
|
45
|
+
{t('customer_accounts.domainMapping.cnameInstruction', 'Add a CNAME record for {hostname} pointing to {target} in your DNS provider', {
|
|
46
|
+
hostname: exampleHost,
|
|
47
|
+
target: cnameTarget ?? '—',
|
|
48
|
+
})}
|
|
49
|
+
</p>
|
|
50
|
+
{cnameTarget ? (
|
|
51
|
+
<div className="flex items-center gap-2 rounded-md border bg-background px-3 py-2">
|
|
52
|
+
<code className="flex-1 truncate font-mono text-sm">{cnameTarget}</code>
|
|
53
|
+
<CopyButton value={cnameTarget} label={t('customer_accounts.domainMapping.copyTarget', 'Copy')} />
|
|
54
|
+
</div>
|
|
55
|
+
) : null}
|
|
56
|
+
</div>
|
|
57
|
+
{aRecordTarget ? (
|
|
58
|
+
<div className="space-y-2">
|
|
59
|
+
<p className="text-sm text-muted-foreground">
|
|
60
|
+
{t('customer_accounts.domainMapping.aRecordInstruction', 'For an apex domain, add an A record for {hostname} pointing to {target}', {
|
|
61
|
+
hostname: exampleHost,
|
|
62
|
+
target: aRecordTarget,
|
|
63
|
+
})}
|
|
64
|
+
</p>
|
|
65
|
+
<div className="flex items-center gap-2 rounded-md border bg-background px-3 py-2">
|
|
66
|
+
<code className="flex-1 truncate font-mono text-sm">{aRecordTarget}</code>
|
|
67
|
+
<CopyButton value={aRecordTarget} label={t('customer_accounts.domainMapping.copyTarget', 'Copy')} />
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
<p className="text-xs text-muted-foreground">
|
|
72
|
+
{t('customer_accounts.domainMapping.propagationNote', 'DNS changes can take up to 48 hours to propagate. The system checks automatically — you\'ll be notified when it\'s ready.')}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default DefaultDnsConfig
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
5
|
+
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { DefaultStepper, type StepperProps } from './Stepper'
|
|
8
|
+
import { DefaultStatusBadge, type StatusBadgeSectionProps } from './StatusBadgeSection'
|
|
9
|
+
import { DefaultDnsConfig, type DnsConfigProps } from './DnsConfig'
|
|
10
|
+
import { DefaultActions, type ActionsProps } from './Actions'
|
|
11
|
+
import { DnsDiagnostics, TlsDiagnostics } from './Diagnostics'
|
|
12
|
+
import type { DomainMappingRow } from './types'
|
|
13
|
+
|
|
14
|
+
export type DomainPanelProps = {
|
|
15
|
+
mapping: DomainMappingRow
|
|
16
|
+
isReplacement: boolean
|
|
17
|
+
cnameTarget: string | null
|
|
18
|
+
aRecordTarget: string | null
|
|
19
|
+
busy?: boolean
|
|
20
|
+
onCheckNow: () => void
|
|
21
|
+
onRetryTls: () => void
|
|
22
|
+
onChangeDomain: () => void
|
|
23
|
+
onRemove: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DomainPanel(props: DomainPanelProps) {
|
|
27
|
+
const t = useT()
|
|
28
|
+
const Stepper = useRegisteredComponent<StepperProps>(
|
|
29
|
+
'section:customer_accounts.domain-settings:stepper',
|
|
30
|
+
DefaultStepper,
|
|
31
|
+
)
|
|
32
|
+
const StatusBadgeSection = useRegisteredComponent<StatusBadgeSectionProps>(
|
|
33
|
+
'section:customer_accounts.domain-settings:status',
|
|
34
|
+
DefaultStatusBadge,
|
|
35
|
+
)
|
|
36
|
+
const DnsConfig = useRegisteredComponent<DnsConfigProps>(
|
|
37
|
+
'section:customer_accounts.domain-settings:dns-config',
|
|
38
|
+
DefaultDnsConfig,
|
|
39
|
+
)
|
|
40
|
+
const Actions = useRegisteredComponent<ActionsProps>(
|
|
41
|
+
'section:customer_accounts.domain-settings:actions',
|
|
42
|
+
DefaultActions,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const { mapping, isReplacement, cnameTarget, aRecordTarget } = props
|
|
46
|
+
const showDnsConfig = mapping.status !== 'active'
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<section className="space-y-4 rounded-xl border bg-card p-6 shadow-sm">
|
|
50
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
51
|
+
<div className="space-y-1">
|
|
52
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
53
|
+
<h3 className="text-lg font-semibold tracking-tight">{mapping.hostname}</h3>
|
|
54
|
+
{isReplacement ? (
|
|
55
|
+
<Tag variant="info" dot>
|
|
56
|
+
{t('customer_accounts.domainMapping.replacement.label', 'Replacement (waiting to activate)')}
|
|
57
|
+
</Tag>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
<StatusBadgeSection mapping={mapping} />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<Stepper status={mapping.status} />
|
|
65
|
+
|
|
66
|
+
<DnsDiagnostics mapping={mapping} />
|
|
67
|
+
<TlsDiagnostics mapping={mapping} />
|
|
68
|
+
|
|
69
|
+
{showDnsConfig ? (
|
|
70
|
+
<DnsConfig
|
|
71
|
+
hostname={mapping.hostname}
|
|
72
|
+
cnameTarget={mapping.cnameTarget ?? cnameTarget}
|
|
73
|
+
aRecordTarget={mapping.aRecordTarget ?? aRecordTarget}
|
|
74
|
+
/>
|
|
75
|
+
) : null}
|
|
76
|
+
|
|
77
|
+
<Actions
|
|
78
|
+
mapping={mapping}
|
|
79
|
+
isReplacement={isReplacement}
|
|
80
|
+
busy={props.busy}
|
|
81
|
+
onCheckNow={props.onCheckNow}
|
|
82
|
+
onRetryTls={props.onRetryTls}
|
|
83
|
+
onChangeDomain={props.onChangeDomain}
|
|
84
|
+
onRemove={props.onRemove}
|
|
85
|
+
/>
|
|
86
|
+
</section>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default DomainPanel
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
|
|
6
|
+
export type OrgOption = {
|
|
7
|
+
id: string
|
|
8
|
+
label: string
|
|
9
|
+
depth: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type OrgSwitcherProps = {
|
|
13
|
+
options: OrgOption[]
|
|
14
|
+
selectedId: string | null
|
|
15
|
+
onChange: (next: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function OrgSwitcher({ options, selectedId, onChange }: OrgSwitcherProps) {
|
|
19
|
+
const t = useT()
|
|
20
|
+
if (options.length <= 1) return null
|
|
21
|
+
return (
|
|
22
|
+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
23
|
+
<span>{t('customer_accounts.domainMapping.orgSwitcher.label', 'Organization')}</span>
|
|
24
|
+
<select
|
|
25
|
+
className="h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground shadow-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
26
|
+
value={selectedId ?? ''}
|
|
27
|
+
onChange={(event) => onChange(event.target.value)}
|
|
28
|
+
>
|
|
29
|
+
{options.map((opt) => {
|
|
30
|
+
const indent = opt.depth > 0 ? ' '.repeat(opt.depth) : ''
|
|
31
|
+
return (
|
|
32
|
+
<option key={opt.id} value={opt.id}>
|
|
33
|
+
{indent}
|
|
34
|
+
{opt.label}
|
|
35
|
+
</option>
|
|
36
|
+
)
|
|
37
|
+
})}
|
|
38
|
+
</select>
|
|
39
|
+
</label>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default OrgSwitcher
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
12
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
13
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
14
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
15
|
+
import { KbdShortcut } from '@open-mercato/ui/primitives/kbd'
|
|
16
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
17
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
|
+
|
|
19
|
+
export type RegisterDialogProps = {
|
|
20
|
+
open: boolean
|
|
21
|
+
onOpenChange: (next: boolean) => void
|
|
22
|
+
mode: 'register' | 'change'
|
|
23
|
+
currentHostname?: string | null
|
|
24
|
+
onSubmit: (hostname: string) => Promise<void>
|
|
25
|
+
initialError?: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HOSTNAME_BASIC = /^[a-z0-9.-]+$/i
|
|
29
|
+
|
|
30
|
+
export function RegisterDialog({
|
|
31
|
+
open,
|
|
32
|
+
onOpenChange,
|
|
33
|
+
mode,
|
|
34
|
+
currentHostname,
|
|
35
|
+
onSubmit,
|
|
36
|
+
initialError,
|
|
37
|
+
}: RegisterDialogProps) {
|
|
38
|
+
const t = useT()
|
|
39
|
+
const [hostname, setHostname] = React.useState('')
|
|
40
|
+
const [error, setError] = React.useState<string | null>(initialError ?? null)
|
|
41
|
+
const [submitting, setSubmitting] = React.useState(false)
|
|
42
|
+
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
43
|
+
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!open) {
|
|
46
|
+
setHostname('')
|
|
47
|
+
setError(null)
|
|
48
|
+
setSubmitting(false)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
setError(initialError ?? null)
|
|
52
|
+
window.setTimeout(() => inputRef.current?.focus(), 0)
|
|
53
|
+
}, [open, initialError])
|
|
54
|
+
|
|
55
|
+
const validate = React.useCallback(
|
|
56
|
+
(value: string): string | null => {
|
|
57
|
+
const trimmed = value.trim()
|
|
58
|
+
if (!trimmed) return t('customer_accounts.domainMapping.errors.required', 'Domain is required')
|
|
59
|
+
if (trimmed.length > 253) return t('customer_accounts.domainMapping.hostname.validation.invalid', 'Enter a valid domain name (no protocol, path, or port)')
|
|
60
|
+
if (trimmed.includes('://') || trimmed.includes('/') || trimmed.includes(':')) {
|
|
61
|
+
return t('customer_accounts.domainMapping.hostname.validation.invalid', 'Enter a valid domain name (no protocol, path, or port)')
|
|
62
|
+
}
|
|
63
|
+
if (!HOSTNAME_BASIC.test(trimmed) && !/^xn--/i.test(trimmed)) {
|
|
64
|
+
return t('customer_accounts.domainMapping.hostname.validation.invalid', 'Enter a valid domain name (no protocol, path, or port)')
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
},
|
|
68
|
+
[t],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const handleSubmit = React.useCallback(
|
|
72
|
+
async (event: React.FormEvent) => {
|
|
73
|
+
event.preventDefault()
|
|
74
|
+
const validation = validate(hostname)
|
|
75
|
+
if (validation) {
|
|
76
|
+
setError(validation)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
setSubmitting(true)
|
|
80
|
+
setError(null)
|
|
81
|
+
try {
|
|
82
|
+
await onSubmit(hostname.trim())
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : 'Submission failed'
|
|
85
|
+
setError(message)
|
|
86
|
+
} finally {
|
|
87
|
+
setSubmitting(false)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[hostname, onSubmit, validate],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const handleKeyDown = React.useCallback(
|
|
94
|
+
(event: React.KeyboardEvent<HTMLFormElement>) => {
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
handleSubmit(event)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[handleSubmit],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const title =
|
|
104
|
+
mode === 'change'
|
|
105
|
+
? t('customer_accounts.domainMapping.dialog.change.title', 'Change custom domain')
|
|
106
|
+
: t('customer_accounts.domainMapping.dialog.register.title', 'Register custom domain')
|
|
107
|
+
|
|
108
|
+
const description =
|
|
109
|
+
mode === 'change' && currentHostname
|
|
110
|
+
? t(
|
|
111
|
+
'customer_accounts.domainMapping.changeDomain.description',
|
|
112
|
+
'Your current domain ({hostname}) will stay active until the new domain is fully set up. No downtime for your customers.',
|
|
113
|
+
{ hostname: currentHostname },
|
|
114
|
+
)
|
|
115
|
+
: t('customer_accounts.domainMapping.description', 'Map your own domain to the customer portal')
|
|
116
|
+
|
|
117
|
+
const submitLabel =
|
|
118
|
+
mode === 'change'
|
|
119
|
+
? t('customer_accounts.domainMapping.dialog.change.submit', 'Start replacement')
|
|
120
|
+
: t('customer_accounts.domainMapping.dialog.register.submit', 'Register')
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
124
|
+
<DialogContent>
|
|
125
|
+
<DialogHeader>
|
|
126
|
+
<DialogTitle>{title}</DialogTitle>
|
|
127
|
+
<DialogDescription>{description}</DialogDescription>
|
|
128
|
+
</DialogHeader>
|
|
129
|
+
<form className="space-y-4" onSubmit={handleSubmit} onKeyDown={handleKeyDown}>
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<Label htmlFor="custom-domain-hostname">
|
|
132
|
+
{t('customer_accounts.domainMapping.hostname.label', 'Domain')}
|
|
133
|
+
</Label>
|
|
134
|
+
<Input
|
|
135
|
+
id="custom-domain-hostname"
|
|
136
|
+
ref={inputRef}
|
|
137
|
+
value={hostname}
|
|
138
|
+
autoComplete="off"
|
|
139
|
+
spellCheck={false}
|
|
140
|
+
placeholder={t('customer_accounts.domainMapping.hostname.placeholder', 'e.g., shop.yourdomain.com')}
|
|
141
|
+
onChange={(event) => {
|
|
142
|
+
setHostname(event.target.value)
|
|
143
|
+
if (error) setError(null)
|
|
144
|
+
}}
|
|
145
|
+
aria-invalid={error ? true : undefined}
|
|
146
|
+
disabled={submitting}
|
|
147
|
+
/>
|
|
148
|
+
{error ? (
|
|
149
|
+
<p className="text-sm text-status-error-text" role="alert">
|
|
150
|
+
{error}
|
|
151
|
+
</p>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
<DialogFooter className="items-center">
|
|
155
|
+
<span className="hidden text-xs text-muted-foreground sm:inline-flex sm:items-center sm:gap-1">
|
|
156
|
+
<KbdShortcut keys={['⌘', 'Enter']} />
|
|
157
|
+
</span>
|
|
158
|
+
<Button
|
|
159
|
+
type="button"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
size="default"
|
|
162
|
+
onClick={() => onOpenChange(false)}
|
|
163
|
+
disabled={submitting}
|
|
164
|
+
>
|
|
165
|
+
{t('customer_accounts.domainMapping.dialog.cancel', 'Cancel')}
|
|
166
|
+
</Button>
|
|
167
|
+
<Button type="submit" size="default" disabled={submitting}>
|
|
168
|
+
{submitting ? <Spinner className="mr-2 h-4 w-4" /> : null}
|
|
169
|
+
{submitLabel}
|
|
170
|
+
</Button>
|
|
171
|
+
</DialogFooter>
|
|
172
|
+
</form>
|
|
173
|
+
</DialogContent>
|
|
174
|
+
</Dialog>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default RegisterDialog
|