Add Apple Pay and Google Pay to Stripe checkout. Use when: (1) adding mobile wallet payments, (2) improving mobile conversion, (3) implementing one-tap checkout. Stripe Payment Request Button automatically detects device capabilities and shows Apple Pay (Safari/iOS) or Google Pay (Chrome/Android).
Users want frictionless mobile checkout with Apple Pay (iOS/Safari) and Google Pay (Android/Chrome). Manual card entry on mobile has high abandonment rates.
Use this skill when:
Key insight: Stripe's Payment Request Button API handles both automatically - no separate integrations needed.
User Device Detection
├─ iOS + Safari → Shows Apple Pay
├─ Android + Chrome → Shows Google Pay
├─ Desktop Chrome → Shows Google Pay
└─ Fallback → Regular card form
Single API handles both wallets + detects availability automatically.
import { PaymentRequestButtonElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { useState, useEffect } from 'react';
export function AppleGooglePayButton({
amount, // in cents
currency = 'usd',
onSuccess,
onError
}: Props) {
const stripe = useStripe();
const [paymentRequest, setPaymentRequest] = useState(null);
const [canMakePayment, setCanMakePayment] = useState(false);
useEffect(() => {
if (!stripe) return;
// Create payment request
const pr = stripe.paymentRequest({
country: 'US',
currency: currency.toLowerCase(),
total: {
label: 'Order Total',
amount: amount, // in cents
},
requestPayerName: true,
requestPayerEmail: true,
});
// Check if Apple Pay or Google Pay is available
pr.canMakePayment().then(result => {
if (result) {
setPaymentRequest(pr);
setCanMakePayment(true);
}
});
// Handle payment method creation
pr.on('paymentmethod', async (event) => {
try {
// Confirm payment on backend
const response = await fetch('/api/confirm-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method_id: event.paymentMethod.id,
amount,
}),
});
const { clientSecret } = await response.json();
// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{ payment_method: event.paymentMethod.id },
{ handleActions: false }
);
if (error) {
event.complete('fail');
onError?.(error.message);
} else {
event.complete('success');
onSuccess?.(paymentIntent);
}
} catch (err) {
event.complete('fail');
onError?.(err.message);
}
});
}, [stripe, amount, currency, onSuccess, onError]);
if (!canMakePayment) {
return null; // Hide button if not available
}
return (
<PaymentRequestButtonElement
options={{ paymentRequest }}
className="payment-request-button"
/>
);
}
// API endpoint: /api/confirm-payment
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(req: Request) {
const { payment_method_id, amount } = await req.json();
try {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment_method: payment_method_id,
confirmation_method: 'manual',
confirm: true,
return_url: `${req.headers.get('origin')}/payment/success`,
});
return Response.json({
clientSecret: paymentIntent.client_secret
});
} catch (error) {
return Response.json({ error: error.message }, { status: 400 });
}
}
Show Payment Request Button above card form:
<div className="payment-section">
{/* Apple Pay / Google Pay - shows automatically if available */}
<AppleGooglePayButton
amount={totalCents}
currency="usd"
onSuccess={handleSuccess}
onError={handleError}
/>
{/* Divider */}
{canMakePayment && (
<div className="payment-divider">
<span>Or pay with card</span>
</div>
)}
{/* Traditional card form - always shown as fallback */}
<CardElement />
<button onClick={handleCardPayment}>Pay ${total}</button>
</div>
.payment-request-button {
/* Stripe automatically styles for Apple Pay (black) or Google Pay (white) */
height: 44px;
margin-bottom: 16px;
}
.payment-divider {
display: flex;
align-items: center;
text-align: center;
margin: 20px 0;
}
.payment-divider::before,
.payment-divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #e0e0e0;
}
.payment-divider span {
padding: 0 10px;
color: #666;
font-size: 14px;
}
| Device/Browser | Shows Apple Pay | Shows Google Pay | Fallback to Card |
|---|---|---|---|
| iPhone Safari | ✅ Yes | ❌ No | ✅ Yes |
| iPhone Chrome | ❌ No | ❌ No | ✅ Yes |
| Android Chrome | ❌ No | ✅ Yes | ✅ Yes |
| Mac Safari | ✅ Yes | ❌ No | ✅ Yes |
| Mac Chrome | ❌ No | ✅ Yes (if signed in) | ✅ Yes |
| Windows Chrome | ❌ No | ✅ Yes (if signed in) | ✅ Yes |
Enable Apple Pay:
https://yourdomain.com/.well-known/apple-developer-merchantid-domain-associationEnable Google Pay:
Apple Pay and Google Pay ONLY work on HTTPS domains.
localhost (works without HTTPS){
"dependencies": {
"@stripe/stripe-js": "^2.0.0",
"@stripe/react-stripe-js": "^2.0.0",
"stripe": "^14.0.0"
}
}
Industry benchmarks:
Coinbase case study (Moonshot):
Cause: Domain not verified or wallet not enrolled
Fix:
apple-developer-merchantid-domain-association file is accessibleCause: User not signed into Google account with payment method
Fix:
Cause: Missing return_url or invalid amount
Fix:
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
payment_method: payment_method_id,
confirmation_method: 'manual',
confirm: true,
return_url: `${origin}/payment/success`, // REQUIRED
});
Files to modify:
apps/web/components/StripePaymentForm.tsx
PaymentRequestButtonElement above card formcanMakePayment()apps/web/app/checkout/page.tsx
<AppleGooglePayButton> componentBackend (no changes needed)
create_checkout_session Edge Function works as-isEstimated implementation time: 30-45 minutes