@open-mercato/checkout 0.6.6-develop.6184.1.b7e55f8d61 → 0.6.6-develop.6201.1.8ceb502c4b
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/dist/modules/checkout/__integration__/TC-CHKT-035.spec.js +2 -2
- package/dist/modules/checkout/__integration__/TC-CHKT-035.spec.js.map +2 -2
- package/dist/modules/checkout/components/PayPage.js +163 -103
- package/dist/modules/checkout/components/PayPage.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/checkout/__integration__/TC-CHKT-035.spec.ts +2 -2
- package/src/modules/checkout/components/PayPage.tsx +160 -99
- package/src/modules/checkout/components/__tests__/PayPage.ds-contract.test.ts +26 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/checkout",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6201.1.8ceb502c4b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,18 +62,18 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@open-mercato/core": "0.6.6-develop.
|
|
66
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
65
|
+
"@open-mercato/core": "0.6.6-develop.6201.1.8ceb502c4b",
|
|
66
|
+
"@open-mercato/ui": "0.6.6-develop.6201.1.8ceb502c4b",
|
|
67
67
|
"bcryptjs": "^3.0.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
71
|
+
"@open-mercato/shared": "0.6.6-develop.6201.1.8ceb502c4b",
|
|
72
72
|
"react": "^19.0.0",
|
|
73
73
|
"react-dom": "^19.0.0"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.6-develop.6201.1.8ceb502c4b",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"@types/react": "^19.2.17",
|
|
79
79
|
"@types/react-dom": "^19.2.3",
|
|
@@ -46,8 +46,8 @@ test.describe('TC-CHKT-035: Terms/privacy links open popup with sanitized markdo
|
|
|
46
46
|
await page.getByRole('button', { name: /close/i }).click()
|
|
47
47
|
await expect(page.getByRole('dialog')).toBeHidden()
|
|
48
48
|
|
|
49
|
-
await page.
|
|
50
|
-
await page.
|
|
49
|
+
await page.getByRole('checkbox', { name: /terms of service/i }).check()
|
|
50
|
+
await page.getByRole('checkbox', { name: /privacy policy/i }).check()
|
|
51
51
|
await page.getByRole('button', { name: /pay now/i }).click()
|
|
52
52
|
await page.waitForURL(new RegExp(`/pay/${link.slug}/success/`))
|
|
53
53
|
|
|
@@ -19,9 +19,18 @@ import { mapCrudServerErrorToFormErrors } from '@open-mercato/ui/backend/utils/s
|
|
|
19
19
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
20
20
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
21
21
|
import { Card, CardContent } from '@open-mercato/ui/primitives/card'
|
|
22
|
+
import { Checkbox } from '@open-mercato/ui/primitives/checkbox'
|
|
22
23
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
23
24
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
24
25
|
import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
26
|
+
import { Radio, RadioGroup } from '@open-mercato/ui/primitives/radio'
|
|
27
|
+
import {
|
|
28
|
+
Select,
|
|
29
|
+
SelectContent,
|
|
30
|
+
SelectItem,
|
|
31
|
+
SelectTrigger,
|
|
32
|
+
SelectValue,
|
|
33
|
+
} from '@open-mercato/ui/primitives/select'
|
|
25
34
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
26
35
|
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
27
36
|
import {
|
|
@@ -270,6 +279,27 @@ const GATEWAY_FORM_HANDLE = ComponentReplacementHandles.section('checkout.pay-pa
|
|
|
270
279
|
const HELP_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'help')
|
|
271
280
|
const FOOTER_HANDLE = ComponentReplacementHandles.section('checkout.pay-page', 'footer')
|
|
272
281
|
|
|
282
|
+
// Checkout supports merchant branding defaults; keep those theme fallbacks isolated from general UI state colors.
|
|
283
|
+
const CHECKOUT_THEME_FALLBACKS = {
|
|
284
|
+
accent: '#1E3A8A',
|
|
285
|
+
accentSecondary: '#F59E0B',
|
|
286
|
+
darkBackground: '#0B1020',
|
|
287
|
+
lightBackground: '#F5EFE6',
|
|
288
|
+
darkText: '#F8FAFC',
|
|
289
|
+
lightText: '#111827',
|
|
290
|
+
darkMutedText: '#CBD5E1',
|
|
291
|
+
lightMutedText: '#6B7280',
|
|
292
|
+
darkSurface: '#111827',
|
|
293
|
+
lightSurface: '#FFFFFF',
|
|
294
|
+
darkShell: '#020617',
|
|
295
|
+
lightShell: '#FFFDF8',
|
|
296
|
+
darkBorder: '#E2E8F0',
|
|
297
|
+
inputBackground: 'rgba(255, 255, 255, 0.96)',
|
|
298
|
+
inputText: '#0F172A',
|
|
299
|
+
} as const
|
|
300
|
+
|
|
301
|
+
const PAY_PAGE_EMPTY_SELECT_VALUE = '__om_checkout_empty__'
|
|
302
|
+
|
|
273
303
|
function clamp(value: number, min: number, max: number) {
|
|
274
304
|
return Math.min(Math.max(value, min), max)
|
|
275
305
|
}
|
|
@@ -391,23 +421,34 @@ function withAlpha(color: string, alpha: number) {
|
|
|
391
421
|
function getContrastText(color: string) {
|
|
392
422
|
const { r, g, b } = hexToRgb(color)
|
|
393
423
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
394
|
-
return luminance > 0.62 ?
|
|
424
|
+
return luminance > 0.62 ? CHECKOUT_THEME_FALLBACKS.inputText : CHECKOUT_THEME_FALLBACKS.darkText
|
|
395
425
|
}
|
|
396
426
|
|
|
397
427
|
function resolveThemeTokens(payload: PayLinkPayload | null, prefersDark: boolean): PayPageThemeTokens {
|
|
398
|
-
const accent = normalizeHexColor(payload?.primaryColor,
|
|
399
|
-
const accentSecondary = normalizeHexColor(payload?.secondaryColor,
|
|
428
|
+
const accent = normalizeHexColor(payload?.primaryColor, CHECKOUT_THEME_FALLBACKS.accent)
|
|
429
|
+
const accentSecondary = normalizeHexColor(payload?.secondaryColor, CHECKOUT_THEME_FALLBACKS.accentSecondary)
|
|
400
430
|
const requestedMode = payload?.themeMode ?? 'auto'
|
|
401
431
|
const mode = requestedMode === 'dark' || (requestedMode === 'auto' && prefersDark) ? 'dark' : 'light'
|
|
402
|
-
const background = normalizeHexColor(
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
432
|
+
const background = normalizeHexColor(
|
|
433
|
+
payload?.backgroundColor,
|
|
434
|
+
mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkBackground : CHECKOUT_THEME_FALLBACKS.lightBackground,
|
|
435
|
+
)
|
|
436
|
+
const text = mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkText : CHECKOUT_THEME_FALLBACKS.lightText
|
|
437
|
+
const mutedText = mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkMutedText : CHECKOUT_THEME_FALLBACKS.lightMutedText
|
|
438
|
+
const surface = mixHexColors(
|
|
439
|
+
background,
|
|
440
|
+
mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkSurface : CHECKOUT_THEME_FALLBACKS.lightSurface,
|
|
441
|
+
mode === 'dark' ? 0.62 : 0.76,
|
|
442
|
+
)
|
|
406
443
|
const surfaceMuted = mixHexColors(accentSecondary, background, mode === 'dark' ? 0.18 : 0.88)
|
|
407
444
|
const surfaceStrong = mixHexColors(accent, background, mode === 'dark' ? 0.22 : 0.9)
|
|
408
|
-
const border = withAlpha(mode === 'dark' ?
|
|
445
|
+
const border = withAlpha(mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkBorder : accent, mode === 'dark' ? 0.16 : 0.14)
|
|
409
446
|
const borderStrong = withAlpha(accent, mode === 'dark' ? 0.34 : 0.22)
|
|
410
|
-
const shellBackground = mixHexColors(
|
|
447
|
+
const shellBackground = mixHexColors(
|
|
448
|
+
background,
|
|
449
|
+
mode === 'dark' ? CHECKOUT_THEME_FALLBACKS.darkShell : CHECKOUT_THEME_FALLBACKS.lightShell,
|
|
450
|
+
mode === 'dark' ? 0.82 : 0.64,
|
|
451
|
+
)
|
|
411
452
|
const heroStart = withAlpha(accent, mode === 'dark' ? 0.26 : 0.16)
|
|
412
453
|
const heroEnd = withAlpha(accentSecondary, mode === 'dark' ? 0.22 : 0.2)
|
|
413
454
|
const errorBase = mode === 'dark' ? '#F87171' : '#DC2626'
|
|
@@ -431,7 +472,7 @@ function resolveThemeTokens(payload: PayLinkPayload | null, prefersDark: boolean
|
|
|
431
472
|
border,
|
|
432
473
|
borderStrong,
|
|
433
474
|
shadow: mode === 'dark'
|
|
434
|
-
? `0 32px 80px ${withAlpha(
|
|
475
|
+
? `0 32px 80px ${withAlpha(CHECKOUT_THEME_FALLBACKS.darkShell, 0.55)}`
|
|
435
476
|
: `0 28px 72px ${withAlpha(accent, 0.12)}`,
|
|
436
477
|
accentShadow: `0 22px 48px ${withAlpha(accent, mode === 'dark' ? 0.3 : 0.22)}`,
|
|
437
478
|
heroBackground: `linear-gradient(145deg, ${heroStart} 0%, ${heroEnd} 100%)`,
|
|
@@ -443,11 +484,19 @@ function resolveThemeTokens(payload: PayLinkPayload | null, prefersDark: boolean
|
|
|
443
484
|
}
|
|
444
485
|
}
|
|
445
486
|
|
|
446
|
-
function buildReadableInputStyle(themeTokens: PayPageThemeTokens, hasError: boolean): React.CSSProperties {
|
|
487
|
+
function buildReadableInputStyle(themeTokens: PayPageThemeTokens, hasError: boolean, disabled = false): React.CSSProperties {
|
|
488
|
+
if (disabled) {
|
|
489
|
+
return {
|
|
490
|
+
background: 'var(--bg-disabled)',
|
|
491
|
+
color: 'var(--text-disabled)',
|
|
492
|
+
borderColor: 'var(--border-disabled)',
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
447
496
|
return {
|
|
448
|
-
background:
|
|
449
|
-
color:
|
|
450
|
-
borderColor: hasError ? themeTokens.errorBorder : withAlpha(
|
|
497
|
+
background: CHECKOUT_THEME_FALLBACKS.inputBackground,
|
|
498
|
+
color: CHECKOUT_THEME_FALLBACKS.inputText,
|
|
499
|
+
borderColor: hasError ? themeTokens.errorBorder : withAlpha(CHECKOUT_THEME_FALLBACKS.inputText, 0.12),
|
|
451
500
|
boxShadow: hasError ? `0 0 0 3px ${themeTokens.errorRing}` : undefined,
|
|
452
501
|
}
|
|
453
502
|
}
|
|
@@ -459,15 +508,6 @@ function buildValidationMessageStyle(themeTokens: PayPageThemeTokens): React.CSS
|
|
|
459
508
|
}
|
|
460
509
|
}
|
|
461
510
|
|
|
462
|
-
function buildValidationNoticeStyle(themeTokens: PayPageThemeTokens): React.CSSProperties {
|
|
463
|
-
return {
|
|
464
|
-
borderColor: themeTokens.errorBorder,
|
|
465
|
-
background: themeTokens.errorSurface,
|
|
466
|
-
color: themeTokens.errorText,
|
|
467
|
-
boxShadow: `0 0 0 1px ${themeTokens.errorSurfaceStrong} inset`,
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
511
|
function formatPublicFieldValue(value: unknown, booleanLabels?: { trueLabel: string; falseLabel: string }): string {
|
|
472
512
|
if (Array.isArray(value)) return value.map((entry) => String(entry)).join(', ')
|
|
473
513
|
if (typeof value === 'boolean') {
|
|
@@ -555,7 +595,7 @@ function buildButtonStyle(themeTokens: PayPageThemeTokens, variant: 'solid' | 'o
|
|
|
555
595
|
}
|
|
556
596
|
}
|
|
557
597
|
|
|
558
|
-
const READABLE_INPUT_CLASSNAME = 'border bg-white/95 text-slate-900 placeholder:text-slate-400 disabled:cursor-not-allowed disabled:
|
|
598
|
+
const READABLE_INPUT_CLASSNAME = 'border bg-white/95 text-slate-900 placeholder:text-slate-400 disabled:cursor-not-allowed disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-text-disabled'
|
|
559
599
|
|
|
560
600
|
export function PayPageSurface({
|
|
561
601
|
previewBanner,
|
|
@@ -774,11 +814,10 @@ export function PayPageCustomerForm({
|
|
|
774
814
|
boxShadow: fieldError ? `0 0 0 2px ${themeTokens.errorRing}` : undefined,
|
|
775
815
|
}}
|
|
776
816
|
>
|
|
777
|
-
<
|
|
778
|
-
type="checkbox"
|
|
817
|
+
<Checkbox
|
|
779
818
|
checked={value === true}
|
|
780
819
|
disabled={inputsLocked}
|
|
781
|
-
|
|
820
|
+
onCheckedChange={(checked) => onFieldChange(field.key, checked === true)}
|
|
782
821
|
/>
|
|
783
822
|
<span className="space-y-1">
|
|
784
823
|
<span className="font-medium">
|
|
@@ -799,27 +838,38 @@ export function PayPageCustomerForm({
|
|
|
799
838
|
{field.required ? ' *' : ''}
|
|
800
839
|
</label>
|
|
801
840
|
{field.kind === 'multiline' ? (
|
|
802
|
-
|
|
841
|
+
<Textarea
|
|
803
842
|
className={READABLE_INPUT_CLASSNAME}
|
|
804
843
|
value={typeof value === 'string' ? value : ''}
|
|
805
844
|
disabled={inputsLocked}
|
|
806
845
|
onChange={(event) => onFieldChange(field.key, event.target.value)}
|
|
807
846
|
placeholder={field.placeholder ?? undefined}
|
|
808
|
-
style={buildReadableInputStyle(themeTokens, Boolean(fieldError))}
|
|
847
|
+
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
809
848
|
/>
|
|
810
849
|
) : field.kind === 'select' || field.kind === 'radio' ? (
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
value={typeof value === 'string' ? value : ''}
|
|
850
|
+
<Select
|
|
851
|
+
value={typeof value === 'string' && value.length > 0 ? value : PAY_PAGE_EMPTY_SELECT_VALUE}
|
|
814
852
|
disabled={inputsLocked}
|
|
815
|
-
|
|
816
|
-
|
|
853
|
+
onValueChange={(nextValue) => {
|
|
854
|
+
onFieldChange(field.key, nextValue === PAY_PAGE_EMPTY_SELECT_VALUE ? '' : nextValue)
|
|
855
|
+
}}
|
|
817
856
|
>
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
857
|
+
<SelectTrigger
|
|
858
|
+
className={`rounded-xl ${READABLE_INPUT_CLASSNAME}`}
|
|
859
|
+
aria-invalid={Boolean(fieldError)}
|
|
860
|
+
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
861
|
+
>
|
|
862
|
+
<SelectValue placeholder={t('checkout.payPage.fields.selectPlaceholder', 'Select...')} />
|
|
863
|
+
</SelectTrigger>
|
|
864
|
+
<SelectContent>
|
|
865
|
+
<SelectItem value={PAY_PAGE_EMPTY_SELECT_VALUE}>
|
|
866
|
+
{t('checkout.payPage.fields.selectPlaceholder', 'Select...')}
|
|
867
|
+
</SelectItem>
|
|
868
|
+
{(field.options ?? []).map((option) => (
|
|
869
|
+
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
|
|
870
|
+
))}
|
|
871
|
+
</SelectContent>
|
|
872
|
+
</Select>
|
|
823
873
|
) : (
|
|
824
874
|
<Input
|
|
825
875
|
className={READABLE_INPUT_CLASSNAME}
|
|
@@ -829,7 +879,7 @@ export function PayPageCustomerForm({
|
|
|
829
879
|
onChange={(event) => onFieldChange(field.key, event.target.value)}
|
|
830
880
|
placeholder={field.placeholder ?? undefined}
|
|
831
881
|
autoComplete={semanticType === 'email' ? 'email' : semanticType === 'phone' ? 'tel' : undefined}
|
|
832
|
-
style={buildReadableInputStyle(themeTokens, Boolean(fieldError))}
|
|
882
|
+
style={buildReadableInputStyle(themeTokens, Boolean(fieldError), inputsLocked)}
|
|
833
883
|
/>
|
|
834
884
|
)}
|
|
835
885
|
</>
|
|
@@ -896,7 +946,7 @@ export function PayPagePricing({
|
|
|
896
946
|
payload.customAmountMin != null ? formatAmount(payload.customAmountMin, payload.customAmountCurrencyCode) : null,
|
|
897
947
|
payload.customAmountMax != null ? formatAmount(payload.customAmountMax, payload.customAmountCurrencyCode) : null,
|
|
898
948
|
].filter(Boolean).join(' - ')}
|
|
899
|
-
style={buildReadableInputStyle(themeTokens, Boolean(fieldErrors.amount))}
|
|
949
|
+
style={buildReadableInputStyle(themeTokens, Boolean(fieldErrors.amount), inputsLocked)}
|
|
900
950
|
/>
|
|
901
951
|
{fieldErrors.amount ? (
|
|
902
952
|
<p className="text-sm" style={buildValidationMessageStyle(themeTokens)}>
|
|
@@ -907,39 +957,46 @@ export function PayPagePricing({
|
|
|
907
957
|
) : null}
|
|
908
958
|
|
|
909
959
|
{payload.pricingMode === 'price_list' ? (
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
960
|
+
<>
|
|
961
|
+
<RadioGroup
|
|
962
|
+
className="space-y-3"
|
|
963
|
+
value={selectedPriceItemId ?? ''}
|
|
964
|
+
disabled={inputsLocked}
|
|
965
|
+
onValueChange={(itemId) => {
|
|
966
|
+
const item = (payload.priceListItems ?? []).find((candidate) => candidate.id === itemId)
|
|
967
|
+
if (item) onPriceItemSelect(item)
|
|
968
|
+
}}
|
|
969
|
+
>
|
|
970
|
+
{(payload.priceListItems ?? []).map((item) => (
|
|
971
|
+
<label
|
|
972
|
+
key={item.id}
|
|
973
|
+
className="flex cursor-pointer items-center justify-between gap-3 rounded-xl border px-4 py-4 text-sm transition"
|
|
974
|
+
style={{
|
|
975
|
+
background: selectedPriceItemId === item.id
|
|
976
|
+
? withAlpha(themeTokens.accent, 0.08)
|
|
977
|
+
: withAlpha(themeTokens.shellBackground, 0.66),
|
|
978
|
+
borderColor: selectedPriceItemId === item.id
|
|
979
|
+
? themeTokens.borderStrong
|
|
980
|
+
: themeTokens.border,
|
|
981
|
+
}}
|
|
982
|
+
>
|
|
983
|
+
<span className="flex items-center gap-3">
|
|
984
|
+
<Radio
|
|
985
|
+
value={item.id}
|
|
986
|
+
disabled={inputsLocked}
|
|
987
|
+
/>
|
|
988
|
+
<span className="font-medium">{item.description}</span>
|
|
989
|
+
</span>
|
|
990
|
+
<span className="whitespace-nowrap font-semibold">{formatAmount(item.amount, item.currencyCode)}</span>
|
|
991
|
+
</label>
|
|
992
|
+
))}
|
|
993
|
+
</RadioGroup>
|
|
937
994
|
{fieldErrors.selectedPriceItemId ? (
|
|
938
995
|
<p className="text-sm" style={buildValidationMessageStyle(themeTokens)}>
|
|
939
996
|
{translateValidationMessage(fieldErrors.selectedPriceItemId, 'selectedPriceItemId')}
|
|
940
997
|
</p>
|
|
941
998
|
) : null}
|
|
942
|
-
|
|
999
|
+
</>
|
|
943
1000
|
) : null}
|
|
944
1001
|
</section>
|
|
945
1002
|
)
|
|
@@ -1017,11 +1074,10 @@ export function PayPageLegalConsent({
|
|
|
1017
1074
|
}}
|
|
1018
1075
|
>
|
|
1019
1076
|
<label className="flex items-start gap-3 text-sm">
|
|
1020
|
-
<
|
|
1021
|
-
type="checkbox"
|
|
1077
|
+
<Checkbox
|
|
1022
1078
|
checked={acceptedLegalConsents.terms === true}
|
|
1023
1079
|
disabled={inputsLocked}
|
|
1024
|
-
|
|
1080
|
+
onCheckedChange={(checked) => onConsentChange('terms', checked === true)}
|
|
1025
1081
|
/>
|
|
1026
1082
|
<span className="space-y-2">
|
|
1027
1083
|
<span className="block">
|
|
@@ -1063,11 +1119,10 @@ export function PayPageLegalConsent({
|
|
|
1063
1119
|
}}
|
|
1064
1120
|
>
|
|
1065
1121
|
<label className="flex items-start gap-3 text-sm">
|
|
1066
|
-
<
|
|
1067
|
-
type="checkbox"
|
|
1122
|
+
<Checkbox
|
|
1068
1123
|
checked={acceptedLegalConsents.privacyPolicy === true}
|
|
1069
1124
|
disabled={inputsLocked}
|
|
1070
|
-
|
|
1125
|
+
onCheckedChange={(checked) => onConsentChange('privacyPolicy', checked === true)}
|
|
1071
1126
|
/>
|
|
1072
1127
|
<span className="space-y-2">
|
|
1073
1128
|
<span className="block">
|
|
@@ -1143,9 +1198,9 @@ export function PayPagePaymentForm({
|
|
|
1143
1198
|
return (
|
|
1144
1199
|
<section className="space-y-4">
|
|
1145
1200
|
{submissionError ? (
|
|
1146
|
-
<
|
|
1147
|
-
{submissionError}
|
|
1148
|
-
</
|
|
1201
|
+
<Alert status="error" style="lighter" size="default">
|
|
1202
|
+
<AlertDescription>{submissionError}</AlertDescription>
|
|
1203
|
+
</Alert>
|
|
1149
1204
|
) : null}
|
|
1150
1205
|
|
|
1151
1206
|
<InjectionSpot spotId="checkout.pay-page:gateway-widget:before" context={injectionContext} />
|
|
@@ -1355,33 +1410,31 @@ export function PayPageFooter({ payload, themeTokens }: PayPageFooterProps) {
|
|
|
1355
1410
|
<div className="flex flex-wrap items-center gap-3 sm:justify-end">
|
|
1356
1411
|
<div className="flex items-center gap-2 text-xs" style={{ color: themeTokens.mutedText }}>
|
|
1357
1412
|
<label htmlFor={selectId}>{t('common.language', 'Language')}</label>
|
|
1358
|
-
<
|
|
1359
|
-
|
|
1413
|
+
<Select
|
|
1414
|
+
value={locale}
|
|
1415
|
+
onValueChange={(nextLocale) => setLocale(nextLocale as Locale)}
|
|
1416
|
+
disabled={pending}
|
|
1417
|
+
>
|
|
1418
|
+
<SelectTrigger
|
|
1360
1419
|
id={selectId}
|
|
1361
|
-
|
|
1420
|
+
size="sm"
|
|
1421
|
+
className="w-auto min-w-28 rounded-full text-xs font-medium"
|
|
1362
1422
|
style={{
|
|
1363
1423
|
background: withAlpha(themeTokens.surface, 0.92),
|
|
1364
1424
|
borderColor: themeTokens.borderStrong,
|
|
1365
1425
|
color: themeTokens.text,
|
|
1366
1426
|
}}
|
|
1367
|
-
value={locale}
|
|
1368
|
-
onChange={(event) => setLocale(event.target.value as Locale)}
|
|
1369
|
-
disabled={pending}
|
|
1370
1427
|
>
|
|
1428
|
+
<SelectValue />
|
|
1429
|
+
</SelectTrigger>
|
|
1430
|
+
<SelectContent>
|
|
1371
1431
|
{locales.map((entry) => (
|
|
1372
|
-
<
|
|
1432
|
+
<SelectItem key={entry} value={entry}>
|
|
1373
1433
|
{languageLabels[entry]}
|
|
1374
|
-
</
|
|
1434
|
+
</SelectItem>
|
|
1375
1435
|
))}
|
|
1376
|
-
</
|
|
1377
|
-
|
|
1378
|
-
aria-hidden="true"
|
|
1379
|
-
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-overline"
|
|
1380
|
-
style={{ color: themeTokens.mutedText }}
|
|
1381
|
-
>
|
|
1382
|
-
▼
|
|
1383
|
-
</span>
|
|
1384
|
-
</div>
|
|
1436
|
+
</SelectContent>
|
|
1437
|
+
</Select>
|
|
1385
1438
|
</div>
|
|
1386
1439
|
<div className="text-xs uppercase tracking-widest">
|
|
1387
1440
|
{payload.gatewayProviderKey ?? t('checkout.payPage.header.autoProvider', 'Configured in checkout')}
|
|
@@ -1967,13 +2020,21 @@ export function PayPage({
|
|
|
1967
2020
|
if (loadError || !payload) {
|
|
1968
2021
|
return (
|
|
1969
2022
|
<div className="mx-auto max-w-3xl px-4 py-16">
|
|
1970
|
-
<Alert
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2023
|
+
<Alert
|
|
2024
|
+
status="error"
|
|
2025
|
+
style="lighter"
|
|
2026
|
+
size="default"
|
|
2027
|
+
action={(
|
|
1974
2028
|
<Button type="button" variant="outline" onClick={() => { void loadPayload() }}>
|
|
1975
2029
|
{t('checkout.payPage.actions.retry', 'Retry')}
|
|
1976
2030
|
</Button>
|
|
2031
|
+
)}
|
|
2032
|
+
>
|
|
2033
|
+
<div className="space-y-1">
|
|
2034
|
+
<AlertTitle>{t('checkout.payPage.errors.loadTitle', 'Unable to load payment link')}</AlertTitle>
|
|
2035
|
+
<AlertDescription>
|
|
2036
|
+
{loadError ?? t('checkout.payPage.errors.loadMessage', "We couldn't load this payment page. Please try again.")}
|
|
2037
|
+
</AlertDescription>
|
|
1977
2038
|
</div>
|
|
1978
2039
|
</Alert>
|
|
1979
2040
|
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
const PAY_PAGE_SOURCE = path.resolve(__dirname, '..', 'PayPage.tsx')
|
|
5
|
+
|
|
6
|
+
function readPayPageSource(): string {
|
|
7
|
+
return fs.readFileSync(PAY_PAGE_SOURCE, 'utf8')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('PayPage DS contract', () => {
|
|
11
|
+
it('uses current DS primitives for standard controls and feedback', () => {
|
|
12
|
+
const source = readPayPageSource()
|
|
13
|
+
|
|
14
|
+
expect(source).not.toMatch(/ErrorNotice/)
|
|
15
|
+
expect(source).not.toMatch(/<input\b/)
|
|
16
|
+
expect(source).not.toMatch(/<select\b/)
|
|
17
|
+
expect(source).not.toContain('disabled:opacity-50')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('keeps merchant theme color defaults isolated as explicit checkout branding exceptions', () => {
|
|
21
|
+
const source = readPayPageSource()
|
|
22
|
+
|
|
23
|
+
expect(source).toContain('CHECKOUT_THEME_FALLBACKS')
|
|
24
|
+
expect(source).toContain('merchant branding defaults')
|
|
25
|
+
})
|
|
26
|
+
})
|