Skip to content

fix: APM phone field — required validation + { dialing_code, number } alignment#273

Draft
roshan-gorasia-cko wants to merge 5 commits into
masterfrom
fix/apm-phone-required-validation
Draft

fix: APM phone field — required validation + { dialing_code, number } alignment#273
roshan-gorasia-cko wants to merge 5 commits into
masterfrom
fix/apm-phone-required-validation

Conversation

@roshan-gorasia-cko

@roshan-gorasia-cko roshan-gorasia-cko commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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 public InitialData.phone_number type used value, while the Phone component emits number after 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: "" }, but validateField only read the value key, so it saw a non-empty object and passed.

  • getComparableFieldValue() (src/apm/utils.ts) normalises a field value to its scalar (prefers number, still reads value); used in validateField (src/apm/views/utils/form.ts).
  • The Phone component emits oninput when 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_number prefill 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 / legacy value into { dialing_code, number }.
  • Seed and prefill (src/apm/views/NextSteps.ts) normalise through it.
  • The prefilled value is now passed to the Phone component 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_number accepts number and keeps value as a deprecated alias, so existing prefills keep working.

Tests

  • Adds Vitest with coverage for value normalisation, the required rule in both shapes, the { dialing_code, number } coercion, and an end-to-end shape check that a merchant { dialing_code, value } prefill is submitted as { dialing_code, number }.
  • Tests live under a top-level test/ directory, outside the SDK build, so no test code ships in the bundle.
  • yarn test is wired into the existing lint/build CI workflow.

roshan-gorasia-cko and others added 2 commits July 22, 2026 17:16
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>
@roshan-gorasia-cko roshan-gorasia-cko changed the title fix: enforce required validation on APM phone field fix: APM phone field — required validation + { dialing_code, number } alignment Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() and normalizePhoneValue() 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.

Comment thread src/apm/utils.ts Outdated
Comment thread src/apm/types.ts
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 normalised actualValue. If a phone value comes through as an object with { number: undefined } (or { value: undefined }), getComparableFieldValue() returns undefined but 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"
      }

Comment thread src/apm/elements/phone.ts
Comment thread src/apm/elements/phone.ts
Comment thread test/support/loadNamespace.ts
roshan-gorasia-cko and others added 2 commits July 22, 2026 19:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants