fix: APM phone field — required validation + { dialing_code, number } alignment#273
fix: APM phone field — required validation + { dialing_code, number } alignment#273roshan-gorasia-cko wants to merge 5 commits into
Conversation
The APM phone form value has two shapes: the initial/prefilled seed stores
the number under `value`, but the Phone component emits it under `number`
once the shopper interacts. The validator only read `value`, so after the
shopper typed a number and removed it the value was `{ number: "" }` — an
object without a `value` key — which the required check treated as a
non-empty value and passed. The required validation effectively disappeared
after any interaction.
- add getComparableFieldValue() in utils.ts to normalise a field value
(reads `value` or `number`) and use it in validateField
- emit oninput when the number is cleared back into the dialing-code prefix
in phone.ts, so a full clear propagates to the form instead of leaving a
stale value
Also adds a Vitest harness (tests under top-level test/, kept out of the SDK
build so nothing ships in dist) with regression coverage, and wires
`yarn test` into the build-lint-test CI workflow.
Note: this harness/CI/version-bump overlaps with the locale-default PR; the
branch that merges second will need a trivial rebase and version re-bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The API reads an APM phone parameter as { dialing_code, number } (api
PhoneValue json tags), but the SDK's seed, prefill and the public
InitialData type used a `value` key. A prefilled phone left untouched was
therefore submitted as { dialing_code, value } and the number was dropped
before reaching the gateway (e.g. MBWay). Only a shopper-typed number
happened to use the correct `number` key.
- add normalizePhoneValue() to coerce a string / `number` / legacy `value`
into the canonical { dialing_code, number } shape
- seed and prefill in NextSteps now normalise via it
- Phone component prop + init read `number`
- InitialData.phone_number accepts `number` (canonical) and keeps `value`
as a deprecated alias, so existing merchant prefills keep working
- getComparableFieldValue prefers `number` (canonical), still reads `value`
Note: this only fixes the submitted value shape. The prefilled number still
does not render in the input (form.ts passes the seed under a `number:` prop
the Phone component ignores, and wiring it through the `value` prop would
trigger the loadScript-per-render mount emit and re-render). Left as a
separate follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes inconsistent APM phone field value shapes by standardizing on { dialing_code, number }, ensuring required validation works after the user clears the input, and preventing prefills from being submitted under the legacy value key. It also introduces a Vitest-based test harness to exercise the real (namespace-style) TypeScript sources without shipping test code in the SDK bundle.
Changes:
- Add
getComparableFieldValue()andnormalizePhoneValue()utilities to normalize phone values for validation and submission. - Update form validation, phone element behavior, and NextSteps seeding/prefill to use the canonical
{ dialing_code, number }shape. - Add Vitest + Node test support and CI wiring, with regression tests covering normalization and required validation.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Adds Vitest and its transitive dependencies to the lockfile. |
| vitest.config.ts | Configures Vitest to run Node-based TypeScript tests under test/**/*.test.ts. |
| test/support/loadNamespace.ts | Provides a harness to transpile and execute namespace-style SDK sources for testing. |
| test/apm/phone-value-shape.test.ts | Adds coverage for normalizePhoneValue() coercion into { dialing_code, number }. |
| test/apm/phone-validation.test.ts | Adds regression coverage for required validation across value vs number shapes. |
| src/apm/views/utils/form.ts | Switches validation to normalize values via getComparableFieldValue(). |
| src/apm/views/NextSteps.ts | Normalizes phone seed/prefill values via normalizePhoneValue(). |
| src/apm/utils.ts | Introduces utilities for comparable validation values and canonical phone normalization. |
| src/apm/types.ts | Updates InitialData.phone_number typing to prefer number and keep value as deprecated alias. |
| src/apm/elements/phone.ts | Aligns Phone element value shape to { dialing_code, number } and propagates clears for validation. |
| package.json | Adds vitest and test scripts; includes tests in verify. |
| eslint.config.mjs | Marks tests and Vitest config as Node-global (not browser-global). |
| .github/workflows/build-lint-test.yml | Runs yarn test in CI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address review feedback: - normalizePhoneValue now coerces a numeric bare/`number`/`value` input to a string instead of dropping it to an empty number - clarify that InitialData.phone_number `number`/`value` are both optional Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/apm/views/utils/form.ts:33
- The required validation still checks
typeof value === "undefined"instead of the normalisedactualValue. If a phone value comes through as an object with{ number: undefined }(or{ value: undefined }),getComparableFieldValue()returnsundefinedbut the required rule will not fire, allowing an empty required phone to pass.
switch (true) {
case validation.required &&
(typeof value === "undefined" || (typeof actualValue === "string" && actualValue.length === 0)): {
return "Missing required value"
}
Address review feedback: the default injected navigator is empty, so pass an explicit navigator for helpers that read from it (e.g. formatCurrency). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Phone component received the seeded value under a `number:` prop it
ignored, so a prefilled number was submitted but never displayed. Pass it
via the `value` prop the component actually reads, and initialise from
`value.number`.
The component's mount callback used to emit an initial oninput whenever a
value prop was present; because loadScript runs that callback on every
render, enabling the value prop would have re-rendered the form in a loop.
Removed that emit — the form value is already seeded by NextSteps, so the
initial sync is redundant.
Adds an end-to-end shape test asserting a merchant `{ dialing_code, value }`
prefill is submitted as `{ dialing_code, number }` with no `value` key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes two related defects in the embedded APM (
client.apm.authorization/apm.tokenization) phone field, both stemming from an inconsistent value shape.Background
The phone parameter is submitted as an object keyed by
{ dialing_code, number }. The SDK was inconsistent about the digits key: the initial seed, prefill, and the publicInitialData.phone_numbertype usedvalue, while thePhonecomponent emitsnumberafter the shopper interacts.Bug 1 — required validation stops working
If a required phone number is entered and then removed, "required" validation stops firing and the field can be submitted empty. After interaction the value is
{ number: "" }, butvalidateFieldonly read thevaluekey, so it saw a non-empty object and passed.getComparableFieldValue()(src/apm/utils.ts) normalises a field value to its scalar (prefersnumber, still readsvalue); used invalidateField(src/apm/views/utils/form.ts).Phonecomponent emitsoninputwhen the number is cleared back into the dialing-code prefix (src/apm/elements/phone.ts), so a full clear propagates to the form instead of leaving a stale value.Bug 2 — prefilled phone number sent under the wrong key (and not displayed)
A
initialData.phone_numberprefill left untouched was submitted as{ dialing_code, value }rather than{ dialing_code, number }, and was not rendered in the input.normalizePhoneValue()(src/apm/utils.ts) coerces a string / number /number/ legacyvalueinto{ dialing_code, number }.src/apm/views/NextSteps.ts) normalise through it.Phonecomponent via the prop it reads, so it renders in the input; the redundant mount-time update that would otherwise re-render in a loop was removed.InitialData.phone_numberacceptsnumberand keepsvalueas a deprecated alias, so existing prefills keep working.Tests
{ dialing_code, number }coercion, and an end-to-end shape check that a merchant{ dialing_code, value }prefill is submitted as{ dialing_code, number }.test/directory, outside the SDK build, so no test code ships in the bundle.yarn testis wired into the existing lint/build CI workflow.