Repository navigation
fix: send exact decimal amounts to Apple Pay and Google Pay - #32
Merged
Merged
Conversation
- `PaymentItem.amount` is now `PaymentAmount` (`number | string`). iOS parses amounts with `NSDecimalNumber(string:)` instead of `Decimal(Double)`, so 19.99 reaches PassKit as 19.99, not 19.98999999999999488. Android parses and sums them as `BigDecimal`. Numbers are read through their shortest round-trip form. - New `normalizePaymentRequest`, used by `createPaymentRequest` and the hook's `startPayment`, rejects NaN, malformed strings, more decimals than the currency allows (ISO 4217) and a negative total before any native call, then sends amounts as decimal strings. Negative line items (discounts) stay allowed. - `calculateTotal`, the hook's `total` and `formatAmount` use exact decimal arithmetic; `formatAmount` accepts strings and a currency. - Native code rejects unparseable amounts and a negative total; Android also rejects a third decimal place instead of rounding. - Docs, README, CHANGELOG and the example app use `formatAmount`. Closes #18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #18
Summary
PaymentItem.amountis nownumber | string(PaymentAmount, a Nitro variant). iOS parses amounts withNSDecimalNumber(string:locale:)instead ofDecimal(Double), so19.99reaches PassKit as19.99, not19.98999999999999488. Android parses and sums them asBigDecimal(newPaymentAmounts.kt). Numbers are read through their shortest round-trip form on both platforms.normalizePaymentRequest(also used bycreatePaymentRequestandusePaymentCheckout().startPayment()) rejectsNaN/Infinity, malformed strings, more decimals than the currency allows (static ISO 4217 table: JPY 0, KWD 3, default 2), and a negative total. It then sends every amount as a decimal string scaled to the currency ('19.90','1500'). Negative line items (discounts) stay allowed. The hook setserrorand returnsnullwithout calling native code, the same way it handles an empty cart.calculateTotal(and the hook'stotal) sum exactly with BigInt and still return anumber, sototal.toFixed(2)keeps working.formatAmount(amount, currencyCode?)accepts strings and rounds half away from zero.buildreported. Android rejects a negativetotalPriceand a third decimal place instead of rounding, and sends whole-number strings (whatnormalizePaymentRequestproduces for JPY) as"1500", not"1500.00". On iOS the check runs before any payment state is stored; on Android the error goes through the existingcatch, which releases the in-progress slot, so a rejected request does not block the next one.types.md(newPaymentAmountsection),utils.md,use-payment-checkout.md,hybrid-payment-handler.md,manual-flow.md,dynamic-cart.md,quick-start.md, README, CHANGELOG, example app.Test plan
cd package && bun run typecheck: passescd package && bunx eslint "src/**/*.{ts,tsx}" --max-warnings 0: passescd package && bun run test:ci: 89/89 pass. New:amounts.test.ts(validator cases, includingNaN,-1,1.005USD,0.1 + 0.2, JPY decimals, KWD 3 decimals), exactcalculateTotal,formatAmountwith strings and currencies, and hook tests asserting that invalid amounts never reachstartPayment, that19.99and'19.99'both cross as'19.99'(with and withoutonAuthorize), and that a rejected cart does not block the next payment../gradlew :gmisoftware_react-native-pay:testDebugUnitTestfromexample/android). NewPaymentAmountsTest(4: number and string parsing, exact sums) and 5 new cases inGooglePayRequestBuilderTest(exacttotalPrice, JPY formatting, a negative discount line, rejection of a negative total and of a third decimal place); its existing cases now build items withPaymentAmount.create(...).swiftscript against a copy of thePaymentAmountenum. No XCTest target exists yet (Add native unit tests and native build jobs to CI #22).19.99→19.99(old:19.98999999999999488),4.99 + "0.50"→5.49(old:5.490000000000001024);NaN,"12abc"," 1.00","1,00","1e3"→ rejected.xcodebuild -workspace example.xcworkspace -scheme example -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO build(afterpod install): succeeds, as does theNitroPayscheme alone, with no warnings in the changed Swift files. The app was not launched (expo run:iosnot run).cd example && bunx expo run:android: not run. The Gradle unit test task compiles the module's Kotlin, but not the C++/JNI glue (the local CMake configure step fails for environment reasons unrelated to this change).bun run docs:build: passes, no broken links or anchorstransactionAmount: not run, needs a merchant certificateRisk
PaymentItem.amountis nownumber | string, so consumer code that readsitem.amountas a number (item.amount.toFixed(2)) no longer compiles. UseformatAmount(item.amount). The example and docs are updated.1.1 * 3=3.3000000000000003) or with amounts more precise than the currency now fail with an error instead of being rounded silently.createPaymentRequestnow throws for invalid amounts and returns string amounts.HybridPaymentHandler.startPaymentcalls are not validated in JS unless the caller usesnormalizePaymentRequest/createPaymentRequest. Native still rejects malformed amounts and negative totals, but on iOS a1.005USD amount passes through.getPaymentHandler()(fix: guard concurrent startPayment calls on Android and in the hook #29) returns the raw hybrid object, so validating direct callers automatically needs a JS wrapper; left as a follow-up.VariantPaymentAmount), so a native rebuild (pod install/ Gradle sync) is needed.JPaymentAmount.cpp/.hpp,JPaymentItem.hpp,NitroPay+autolinking.cmake) is unmodified Nitrogen output but was not built here. The shared C++PaymentItem.hppvariant is compiled by the iOS build.Notes
numberamounts (unlike the issue's suggestion): numbers are exact for any literal amount, and the precision check catches float arithmetic noise, so a warning would fire on every docs example without improving correctness.DisplayItem.price, and PassKit allows negative summary items); only a negative total is rejected. A single-1item is therefore still rejected.Errors with descriptive messages. Typed configuration errors are Typed payment status and error codes; stop treating cancellation as an error #15.amountMinor) are not added; decimal strings cover server-provided amounts. Possible follow-up.Need help on this PR? Tag
@codesmith-botwith what you need.