@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/checkout",
3
- "version": "0.6.6-develop.6184.1.b7e55f8d61",
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.6184.1.b7e55f8d61",
66
- "@open-mercato/ui": "0.6.6-develop.6184.1.b7e55f8d61",
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.6184.1.b7e55f8d61",
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.6184.1.b7e55f8d61",
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.locator('input[type="checkbox"]').nth(0).check()
50
- await page.locator('input[type="checkbox"]').nth(1).check()
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 ? '#0F172A' : '#F8FAFC'
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, '#1E3A8A')
399
- const accentSecondary = normalizeHexColor(payload?.secondaryColor, '#F59E0B')
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(payload?.backgroundColor, mode === 'dark' ? '#0B1020' : '#F5EFE6')
403
- const text = mode === 'dark' ? '#F8FAFC' : '#111827'
404
- const mutedText = mode === 'dark' ? '#CBD5E1' : '#6B7280'
405
- const surface = mixHexColors(background, mode === 'dark' ? '#111827' : '#FFFFFF', mode === 'dark' ? 0.62 : 0.76)
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' ? '#E2E8F0' : accent, mode === 'dark' ? 0.16 : 0.14)
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(background, mode === 'dark' ? '#020617' : '#FFFDF8', mode === 'dark' ? 0.82 : 0.64)
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('#020617', 0.55)}`
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: 'rgba(255, 255, 255, 0.96)',
449
- color: '#0F172A',
450
- borderColor: hasError ? themeTokens.errorBorder : withAlpha('#0F172A', 0.12),
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:opacity-50'
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
- <input
778
- type="checkbox"
817
+ <Checkbox
779
818
  checked={value === true}
780
819
  disabled={inputsLocked}
781
- onChange={(event) => onFieldChange(field.key, event.target.checked)}
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
- <Textarea
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
- <select
812
- className={`w-full rounded-xl px-3 py-2.5 text-sm ${READABLE_INPUT_CLASSNAME}`}
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
- onChange={(event) => onFieldChange(field.key, event.target.value)}
816
- style={buildReadableInputStyle(themeTokens, Boolean(fieldError))}
853
+ onValueChange={(nextValue) => {
854
+ onFieldChange(field.key, nextValue === PAY_PAGE_EMPTY_SELECT_VALUE ? '' : nextValue)
855
+ }}
817
856
  >
818
- <option value="">{t('checkout.payPage.fields.selectPlaceholder', 'Select...')}</option>
819
- {(field.options ?? []).map((option) => (
820
- <option key={option.value} value={option.value}>{option.label}</option>
821
- ))}
822
- </select>
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
- <div className="space-y-3">
911
- {(payload.priceListItems ?? []).map((item) => (
912
- <label
913
- key={item.id}
914
- className="flex cursor-pointer items-center justify-between gap-3 rounded-xl border px-4 py-4 text-sm transition"
915
- style={{
916
- background: selectedPriceItemId === item.id
917
- ? withAlpha(themeTokens.accent, 0.08)
918
- : withAlpha(themeTokens.shellBackground, 0.66),
919
- borderColor: selectedPriceItemId === item.id
920
- ? themeTokens.borderStrong
921
- : themeTokens.border,
922
- }}
923
- >
924
- <span className="flex items-center gap-3">
925
- <input
926
- type="radio"
927
- name="priceItem"
928
- checked={selectedPriceItemId === item.id}
929
- disabled={inputsLocked}
930
- onChange={() => onPriceItemSelect(item)}
931
- />
932
- <span className="font-medium">{item.description}</span>
933
- </span>
934
- <span className="whitespace-nowrap font-semibold">{formatAmount(item.amount, item.currencyCode)}</span>
935
- </label>
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
- </div>
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
- <input
1021
- type="checkbox"
1077
+ <Checkbox
1022
1078
  checked={acceptedLegalConsents.terms === true}
1023
1079
  disabled={inputsLocked}
1024
- onChange={(event) => onConsentChange('terms', event.target.checked)}
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
- <input
1067
- type="checkbox"
1122
+ <Checkbox
1068
1123
  checked={acceptedLegalConsents.privacyPolicy === true}
1069
1124
  disabled={inputsLocked}
1070
- onChange={(event) => onConsentChange('privacyPolicy', event.target.checked)}
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
- <div className="rounded-xl border px-4 py-3 text-sm" style={buildValidationNoticeStyle(themeTokens)}>
1147
- {submissionError}
1148
- </div>
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
- <div className="relative">
1359
- <select
1413
+ <Select
1414
+ value={locale}
1415
+ onValueChange={(nextLocale) => setLocale(nextLocale as Locale)}
1416
+ disabled={pending}
1417
+ >
1418
+ <SelectTrigger
1360
1419
  id={selectId}
1361
- className="appearance-none rounded-full border px-3 py-1.5 pr-8 text-xs font-medium shadow-sm outline-none disabled:cursor-not-allowed disabled:opacity-50"
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
- <option key={entry} value={entry}>
1432
+ <SelectItem key={entry} value={entry}>
1373
1433
  {languageLabels[entry]}
1374
- </option>
1434
+ </SelectItem>
1375
1435
  ))}
1376
- </select>
1377
- <span
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 variant="destructive">
1971
- <AlertTitle>{t('checkout.payPage.errors.loadTitle', 'Unable to load payment link')}</AlertTitle>
1972
- <AlertDescription>{loadError ?? t('checkout.payPage.errors.loadMessage', "We couldn't load this payment page. Please try again.")}</AlertDescription>
1973
- <div className="mt-2">
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
+ })