- We ran 12 moderated user tests on a React Native + Stripe checkout flow. Six users completed on the first try; six hit visible friction.
- Double-taps happened because the payment sheet took 300–500ms to init on tap. Fix: init on the previous screen's mount.
- Total mismatches (app showed $42, sheet showed $39) made users back out. Match totals to the cent.
- Button copy matters: "Pay $42" beat "Buy Now" beat "Continue to Payment."
- Users look right above the button in the last second before tapping — put price, item, and trust anchors there.
We ran 12 moderated user tests on a checkout flow in a React Native app last month. Same app, same product, same price. The task was simple: buy the thing.
Six of the twelve completed on the first try. Six had visible friction. This post is what we found and what we changed.
The setup
React Native + Expo + Stripe PaymentSheet. A physical iPhone 13 and Pixel 6a, five sessions per device plus two on an older Android. Screen recording and voice-over throughout. The button that opened the payment sheet was labeled "Continue to Payment" at the start of the study.
Finding 1 — the double-tap
Five of the twelve users tapped the payment button twice. Not deliberately — the first tap didn't visibly do anything for 300–500ms while the sheet initialized, so they tapped again thinking they'd missed. On slower devices, the second tap opened a second Stripe sheet on top of the first.
The fix is straightforward and undocumented: initialize the sheet on the previous screen's mount, not on the button press. When the user reaches the checkout screen, the sheet is already ready. The tap opens it instantly — no spinner, no double-tap.
React.useEffect(() => {
initPaymentSheet({ paymentIntentClientSecret });
}, []);
Finding 2 — the total mismatch
Three of the twelve backed out at the payment sheet. The reason: our app showed $42, the Stripe sheet showed $39. The app was rendering with-tax; the sheet was showing pre-tax because we hadn't passed the tax breakdown. One user said out loud: "wait, is this the wrong item?"
The lesson: whatever total your app shows must match the sheet's total to the cent. Either pass the tax in the PaymentIntent, or show pre-tax on the checkout screen and let the sheet add tax. Never let the two disagree.
Finding 3 — button copy
We ran a mini A/B on the button that opens the sheet: "Continue to Payment" vs. "Buy Now" vs. "Pay $42." Six users saw each variant across the sessions.
Rough result: "Pay $42" converted best — specific, action-oriented, no ambiguity. "Buy Now" was next. "Continue to Payment" was the worst: the word "continue" made users expect another step, so they hesitated.
Finding 4 — where users look right before tapping
Eye-tracking would've been nice; we used the think-aloud protocol instead. Consistently, users glanced at the area immediately above the button in the last second before tapping. That's where they wanted the price, the item name, and any trust anchors (card logos, "Secured by Stripe").
Not at the top of the screen. Not in the header. Right above the button. Design that area like it's the last decision surface — because it is.
Finding 5 — the receipt matters
Half the users, once payment succeeded, immediately swiped to close the app and check their email for a receipt. If the receipt didn't arrive in the first 5 seconds, they came back to the app anxious.
We reduced this by showing a "Receipt sent to X" toast on the success screen, with the email address visible.
The checklist you can steal
- Init PaymentSheet on mount, not on tap.
- Match totals exactly between the app and the sheet.
- Use action-specific button copy — "Pay $42" beats "Continue to Payment."
- Put price, item, and trust anchors immediately above the payment button.
- Show a "receipt sent to X" confirmation before the user swipes away.
None of these are surprising in retrospect. All of them showed up in user testing before we noticed them in code — which is the whole argument for testing checkout with real people before you ship it.
If you'd rather start from a checkout that already bakes these patterns in, RapidNative generates React Native + Stripe flows with the payment sheet, total handling, and success states wired up.
What's the worst checkout friction you've hit as a user? Drop a comment.
Top comments (0)