Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ All notable changes to this project are documented in this file.
Pay prices for zero-decimal currencies validated by `normalizePaymentRequest` are sent without
decimals (`"1500"` instead of `"1500.00"`), and Android rejects a negative total or a third
decimal place instead of rounding.
- **iOS: the Apple Pay sheet now names the merchant in the total.** PassKit shows the last summary
item as the total ("Pay *label*"). A one-item cart sent that item as the total, so the sheet read
"Pay Coffee"; a cart with several items and no `merchantName` read "Pay Total". The library now
always appends a total labelled with `merchantName`, and falls back to the app's display name
(`CFBundleDisplayName`, then `CFBundleName`). A one-item cart now shows the item line and then
"Pay *merchant*". If you passed your own "Total" item, remove it: it now appears as a line item
above the total.
- **Installing the package no longer runs `tsc` in your project.** 0.0.17 and earlier shipped a
`postinstall` script (`tsc || exit 0`). The package does not include its own `tsconfig.json`, so
`tsc` picked up the app's `tsconfig.json` and type-checked the whole app on every install; with a
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ function CheckoutScreen() {
isProcessing,
error,
} = usePaymentCheckout({
merchantName: 'Your Store',
currencyCode: 'USD',
countryCode: 'US',
})
Expand Down Expand Up @@ -360,7 +361,7 @@ The all-in-one hook for handling payments. Manages availability checking, cart s

```typescript
interface UsePaymentCheckoutConfig {
merchantName?: string;
merchantName?: string; // "Pay <merchantName>" on Apple Pay; defaults to the app name
countryCode?: string; // Default: 'US'
currencyCode?: string; // Default: 'USD'
supportedNetworks?: string[]; // Default: ['visa', 'mastercard', 'amex', 'discover']
Expand Down Expand Up @@ -410,6 +411,7 @@ interface UsePaymentCheckoutReturn {

```typescript
const checkout = usePaymentCheckout({
merchantName: "Your Store",
currencyCode: "USD",
countryCode: "US",
googlePayMerchantId: "your_google_pay_merchant_id",
Expand Down Expand Up @@ -467,16 +469,19 @@ Start a payment request.
const result = await HybridPaymentHandler.startPayment(
// Validates amounts before the sheet opens; see normalizePaymentRequest
normalizePaymentRequest({
merchantName: "Your Store",
countryCode: "US",
currencyCode: "USD",
merchantCapabilities: ["3DS"],
supportedNetworks: ["visa", "mastercard", "amex"],
paymentItems: [{ label: "Total", amount: "29.99", type: "final" }],
paymentItems: [{ label: "Coffee Subscription", amount: "29.99", type: "final" }],
googlePayMerchantId: "your_google_pay_merchant_id",
})
);
```

On iOS, the Apple Pay sheet lists your `paymentItems` and then a total labelled with `merchantName` ("Pay Your Store"). The library adds that total for you, so don't add your own "Total" item.

**Options:**

```typescript
Expand Down Expand Up @@ -614,6 +619,7 @@ import { createPaymentRequest } from "@gmisoftware/react-native-pay";
const request = createPaymentRequest({
amount: 29.99,
label: "Coffee Subscription",
merchantName: "Your Store",
countryCode: "US",
currencyCode: "USD",
googlePayMerchantId: "your_google_pay_merchant_id",
Expand Down Expand Up @@ -695,7 +701,7 @@ interface PaymentItem {
// Payment Request
interface PaymentRequest {
applePayMerchantIdentifier?: string;
merchantName?: string;
merchantName?: string; // "Pay <merchantName>" on Apple Pay; defaults to the app name
countryCode: string;
currencyCode: string;
paymentItems: PaymentItem[];
Expand Down Expand Up @@ -829,6 +835,7 @@ if (!status.canMakePayments) {
const paymentRequest = createPaymentRequest({
amount: 99.99,
label: 'Premium Subscription',
merchantName: 'Your Store',
countryCode: 'US',
currencyCode: 'USD',
googlePayMerchantId: 'your_google_pay_merchant_id',
Expand Down Expand Up @@ -873,6 +880,7 @@ return Platform.OS === 'ios' ? (

```typescript
const checkout = usePaymentCheckout({
merchantName: "Your Store",
currencyCode: "USD",
});

Expand Down
5 changes: 4 additions & 1 deletion docs/docs/api/hybrid-payment-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ import { HybridPaymentHandler, normalizePaymentRequest } from '@gmisoftware/reac

const result = await HybridPaymentHandler.startPayment(
normalizePaymentRequest({
merchantName: 'Your Store',
countryCode: 'US',
currencyCode: 'USD',
merchantCapabilities: ['3DS'],
supportedNetworks: ['visa', 'mastercard', 'amex'],
paymentItems: [{ label: 'Total', amount: '29.99', type: 'final' }],
paymentItems: [{ label: 'Coffee Subscription', amount: '29.99', type: 'final' }],
googlePayMerchantId: 'your_google_pay_merchant_id',
})
)
Expand Down Expand Up @@ -74,6 +75,8 @@ See [Charge before the sheet closes](/docs/guides/server-processing#charge-befor

On iOS, the merchant ID is resolved from the app entitlements by default. Use `applePayMerchantIdentifier` only when you need to override the configured value at runtime.

On iOS, the Apple Pay sheet lists your `paymentItems` and then a total labelled with `merchantName` ("Pay Your Store"). The library adds that total for you, so don't add your own "Total" item. Without `merchantName`, the total uses your app's display name.

Build the request yourself or use [createPaymentRequest](/docs/api/utils#createpaymentrequestoptions) for a single line item with defaults.

Only one payment can be in flight at a time. A call made while another payment is still in progress, including one started by `usePaymentCheckout`, resolves immediately with `{ success: false, error: 'A payment is already in progress' }` and does not affect the payment in flight. This applies on iOS and Android. With `onAuthorize`, the payment stays in flight until your callback settles.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ interface PaymentRequest {

On iOS, `applePayMerchantIdentifier` is optional and only needed when you want to override the Merchant ID configured in the native app entitlements.

`merchantName` is the name shown on the Google Pay sheet and the label of the Apple Pay total (when the cart has more than one item). If it is missing or blank, Android uses the app's label from the manifest; iOS labels the total "Total".
`merchantName` is the business name shown to the payer: the name on the Google Pay sheet and the label of the Apple Pay total. Apple Pay shows the last summary item as the total ("Pay *label*"), so on iOS the library appends a total item labelled with `merchantName` after your `paymentItems`, also when there is only one item. Don't add your own "Total" item. If `merchantName` is missing or blank, Android uses the app's label from the manifest and iOS uses the app's display name (`CFBundleDisplayName`, then `CFBundleName`).

---

Expand Down
4 changes: 3 additions & 1 deletion docs/docs/api/use-payment-checkout.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Source of truth for these types: [`package/src/hooks/usePaymentCheckout.ts`](htt

```ts
interface UsePaymentCheckoutConfig {
merchantName?: string
merchantName?: string // business name shown on the sheet
countryCode?: string // default: 'US'
currencyCode?: string // default: 'USD'
supportedNetworks?: string[] // default: ['visa', 'mastercard', 'amex', 'discover']
Expand All @@ -32,6 +32,7 @@ interface UsePaymentCheckoutConfig {
}
```

- **merchantName** — The business name shown to the payer. On iOS, Apple Pay labels the total with it, so the sheet reads "Pay *merchantName*". On Android, it is the merchant name on the Google Pay sheet. Without it, iOS uses your app's display name (`CFBundleDisplayName`) and Android uses the app's label. Set it on both platforms.
- **onAuthorize** — Called with the token after the user authorizes the payment. Charge it on your server and resolve with `{ status: 'success' }` or `{ status: 'failure', error }`. See [Charge before the sheet closes](/docs/guides/server-processing#charge-before-the-sheet-closes).

## Return value
Expand Down Expand Up @@ -94,6 +95,7 @@ const {
error,
reset,
} = usePaymentCheckout({
merchantName: 'Your Store',
currencyCode: 'USD',
countryCode: 'US',
googlePayMerchantId: 'your_google_pay_merchant_id',
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { createPaymentRequest } from '@gmisoftware/react-native-pay'
const request = createPaymentRequest({
amount: 29.99, // or '29.99'
label: 'Coffee Subscription',
merchantName: 'Your Store',
countryCode: 'US',
currencyCode: 'USD',
// Optional: applePayMerchantIdentifier, googlePayMerchantId,
Expand Down
1 change: 1 addition & 0 deletions docs/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
isProcessing,
reset,
} = usePaymentCheckout({
merchantName: 'Your Store',
countryCode: 'US',
currencyCode: 'USD',
})
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/guides/dynamic-cart.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,5 @@ const handleClearCart = () => {
- **total** is derived from `items`; you don’t set it manually. It is an exact decimal sum.
- **Round computed amounts** with `formatAmount(value, currencyCode)`. `startPayment()` rejects amounts with more decimal places than the currency allows, which includes unrounded float results such as `1.1 * 3`. See [`PaymentAmount`](/docs/api/types#paymentamount).
- **Discounts** can be negative line items, as long as the total is not negative.
- Use **type: 'final'** for confirmed amounts (e.g. tax, total); use **'pending'** for estimates (e.g. shipping) if your flow supports it.
- Use **type: 'final'** for confirmed amounts (e.g. product price, tax); use **'pending'** for estimates (e.g. shipping) if your flow supports it.
- **One pending item makes the total an estimate.** Google Pay shows the total as estimated (`totalPriceStatus: 'ESTIMATED'`) and Apple Pay marks the total as pending. An estimate does not let you charge a different amount: Google Pay requires the amount you authorize and charge to match the `totalPrice` shown on the sheet, and a mismatch can be declined. If the final amount differs from the estimate, start a new payment with the final amount, unless your gateway explicitly supports adjusting it (for example, incremental authorization).
1 change: 1 addition & 0 deletions docs/docs/guides/manual-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { createPaymentRequest, HybridPaymentHandler } from '@gmisoftware/react-n
const paymentRequest = createPaymentRequest({
amount: 99.99,
label: 'Premium Subscription',
merchantName: 'Your Store',
countryCode: 'US',
currencyCode: 'USD',
googlePayMerchantId: 'your_google_pay_merchant_id',
Expand Down
1 change: 1 addition & 0 deletions docs/docs/integrations/przelewy24.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {

export function CheckoutWithPrzelewy24() {
const {startPayment, canMakePayments, isProcessing} = usePaymentCheckout({
merchantName: 'Your Store',
countryCode: 'PL',
currencyCode: 'PLN',
googlePayMerchantId: 'your_google_pay_merchant_id',
Expand Down
1 change: 1 addition & 0 deletions docs/docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function CheckoutScreen() {
isProcessing,
error,
} = usePaymentCheckout({
merchantName: 'Your Store',
currencyCode: 'USD',
countryCode: 'US',
})
Expand Down
16 changes: 15 additions & 1 deletion docs/docs/setup/ios-apple-pay.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,21 @@ The runtime API reads this Merchant ID from the native app entitlements automati

If you are integrating with Przelewy24, see [Przelewy24](/docs/integrations/przelewy24) for an end-to-end app + backend flow.

## 3. Prebuild and run
## 3. Name your business on the sheet

Apple Pay shows the total as "Pay *merchant*". Pass your business name as `merchantName`:

```ts
usePaymentCheckout({
merchantName: 'Your Store',
currencyCode: 'USD',
countryCode: 'US',
})
```

The sheet lists your cart items and then the total line "Pay Your Store", which the library adds for you. If you omit `merchantName`, the total uses your app's display name (`CFBundleDisplayName`).

## 4. Prebuild and run

```bash
npx expo prebuild --clean
Expand Down
1 change: 1 addition & 0 deletions example/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default function TabOneScreen() {
error,
reset,
} = usePaymentCheckout({
merchantName: "React Native Pay Demo",
countryCode: "US",
currencyCode: "USD",
// Stands in for your backend charging the token while the sheet waits.
Expand Down
33 changes: 20 additions & 13 deletions package/ios/HybridPaymentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ private typealias AuthorizeCallback = (PaymentToken) -> Promise<Promise<PaymentA

private enum BundleKey {
static let applePayMerchantIdentifiers = "ReactNativePayApplePayMerchantIdentifiers"
static let displayName = "CFBundleDisplayName"
static let bundleName = "CFBundleName"
}

// MARK: - Payment Request Builder
Expand Down Expand Up @@ -107,27 +109,32 @@ private struct PaymentRequestBuilder {
return pkItem
}

guard request.paymentItems.count > 1 else {
return lineItems
}

let totalLabel: String
if let merchantName = request.merchantName?
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
!merchantName.isEmpty {
totalLabel = merchantName
} else {
totalLabel = "Total"
}
// PassKit shows the last item as the grand total ("Pay <label>"), so
// always append one labelled with the merchant, even for a single item.
let totalItem = PKPaymentSummaryItem(
label: totalLabel,
label: resolveMerchantName(from: request),
amount: NSDecimalNumber(decimal: totalAmount)
)
// The total is only as final as its least final item.
totalItem.type = request.paymentItems.contains { $0.type == .pending } ? .pending : .final

return lineItems + [totalItem]
}

private static func resolveMerchantName(from request: PaymentRequest) -> String {
let candidates = [
request.merchantName,
Bundle.main.object(forInfoDictionaryKey: BundleKey.displayName) as? String,
Bundle.main.object(forInfoDictionaryKey: BundleKey.bundleName) as? String,
]
for candidate in candidates {
if let name = candidate?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines),
!name.isEmpty {
return name
}
}
return ProcessInfo.processInfo.processName
}

private static func buildMerchantCapabilities(_ capabilities: [String]) -> PKMerchantCapability {
var merchantCapabilities: PKMerchantCapability = []
Expand Down
8 changes: 6 additions & 2 deletions package/src/hooks/usePaymentCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ import {
*/
export interface UsePaymentCheckoutConfig {
/**
* Merchant name shown by payment providers that support displaying it.
* Used primarily by Google Pay.
* Business name shown to the payer on both platforms.
* On iOS it labels the Apple Pay total, so the sheet reads "Pay <merchantName>".
* On Android it is sent to Google Pay as `merchantInfo.merchantName`.
* When omitted, iOS uses the app's display name (`CFBundleDisplayName`) and
* Android uses the app's label.
*/
merchantName?: string

Expand Down Expand Up @@ -158,6 +161,7 @@ export interface UsePaymentCheckoutReturn {
* isProcessing,
* error,
* } = usePaymentCheckout({
* merchantName: 'Your Store',
* currencyCode: 'USD',
* countryCode: 'US',
* googlePayMerchantId: 'your_google_pay_merchant_id',
Expand Down
2 changes: 2 additions & 0 deletions package/src/types/Payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ export interface PaymentRequest {
// Optional Apple Pay override for apps with multiple configured merchant IDs.
// When omitted, iOS resolves the first configured entitlement automatically.
applePayMerchantIdentifier?: string
// Business name shown to the payer. iOS labels the Apple Pay total with it
// ("Pay <merchantName>"); both platforms fall back to the app's name.
merchantName?: string
countryCode: string
currencyCode: string
Expand Down
4 changes: 3 additions & 1 deletion package/src/utils/paymentHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
*
* const request = createPaymentRequest({
* amount: 29.99,
* label: 'Coffee Subscription'
* label: 'Coffee Subscription',
* merchantName: 'Your Store'
* })
* ```
*/
Expand Down Expand Up @@ -126,6 +127,7 @@ export function calculateTotal(items: PaymentItem[]): number {
* const request = createPaymentRequest({
* amount: 29.99,
* label: 'Coffee Subscription',
* merchantName: 'Your Store',
* countryCode: 'CA', // override default
* currencyCode: 'CAD' // override default
* })
Expand Down
Loading