@@ -17,12 +17,14 @@ PayPal Checkout V6 - Payment integration with multiple flow types.
1717
1818**Payment Flow Types:**
1919- **One-Time Payments**: Best for infrequent payments with higher AOV (retail, e-commerce)
20+ - **Checkout with Vault**: Single flow for payment + vault consent (subscriptions with initial charge)
2021- **Vaulted Payments**: Ideal for high-frequency, low-AOV purchases (food delivery, marketplaces)
2122- **Recurring Payments**: Perfect for subscriptions and automated billing (SaaS, streaming, utilities)
2223- **Vault-Initiated Checkout**: Use a previously vaulted PayPal account for subsequent payments
2324
2425**Implementation Features:**
2526- One-time payment session creation
27+ - Checkout with vault session (payment + save)
2628- Billing agreement/vault flow
2729- Vault-initiated checkout
2830- Payment tokenization
@@ -1093,3 +1095,211 @@ export const LineItemsAndShipping: StoryObj = {
10931095 [ "client.min.js" , "paypal-checkout-v6.min.js" ]
10941096 ) ,
10951097} ;
1098+
1099+ // Checkout with Vault Story
1100+ const createCheckoutWithVaultForm = ( ) : HTMLElement => {
1101+ const container = document . createElement ( "div" ) ;
1102+ container . innerHTML = `
1103+ <div class="shared-container paypal-container">
1104+ <h2>PayPal V6 Checkout with Vault</h2>
1105+
1106+ <div class="paypal-description">
1107+ <p class="shared-description">
1108+ Create a one-time payment while simultaneously saving the PayPal account
1109+ for future transactions. The returned nonce can be used for both the
1110+ immediate charge and future recurring payments.
1111+ </p>
1112+
1113+ <div class="paypal-form-group">
1114+ <label class="paypal-checkbox-label">
1115+ <input type="checkbox" id="advancedOptionsToggle" class="paypal-checkbox" />
1116+ <span class="paypal-checkbox-text">Include line items and shipping options</span>
1117+ </label>
1118+ </div>
1119+ </div>
1120+
1121+ <div id="paypal-button" class="paypal-button-container"></div>
1122+
1123+ <div id="result" class="shared-result"></div>
1124+ </div>
1125+ ` ;
1126+
1127+ return container ;
1128+ } ;
1129+
1130+ const CHECKOUT_WITH_VAULT_BASIC = {
1131+ amount : "10.00" ,
1132+ currency : "USD" ,
1133+ intent : "capture" ,
1134+ billingAgreementDetails : {
1135+ description : "Monthly subscription to Totally Real Products!" ,
1136+ } ,
1137+ } ;
1138+
1139+ const CHECKOUT_WITH_VAULT_ADVANCED = {
1140+ amount : "20.00" ,
1141+ currency : "USD" ,
1142+ intent : "capture" ,
1143+ billingAgreementDetails : {
1144+ description : "Premium subscription service with initial payment" ,
1145+ } ,
1146+ lineItems : [
1147+ {
1148+ quantity : "1" ,
1149+ unitAmount : "15.00" ,
1150+ name : "Premium Subscription (First Month)" ,
1151+ kind : "debit" ,
1152+ } ,
1153+ ] ,
1154+ shippingOptions : [
1155+ {
1156+ id : "standard" ,
1157+ label : "Standard Shipping" ,
1158+ selected : true ,
1159+ type : "SHIPPING" ,
1160+ amount : {
1161+ currency : "USD" ,
1162+ value : "5.00" ,
1163+ } ,
1164+ } ,
1165+ {
1166+ id : "express" ,
1167+ label : "Express Shipping" ,
1168+ selected : false ,
1169+ type : "SHIPPING" ,
1170+ amount : {
1171+ currency : "USD" ,
1172+ value : "10.00" ,
1173+ } ,
1174+ } ,
1175+ ] ,
1176+ amountBreakdown : {
1177+ itemTotal : "15.00" ,
1178+ shipping : "5.00" ,
1179+ } ,
1180+ } ;
1181+
1182+ const setupCheckoutWithVault = async (
1183+ container : HTMLElement
1184+ ) : Promise < void > => {
1185+ const clientToken = await getClientToken ( ) ;
1186+ const resultDiv = container . querySelector ( "#result" ) as HTMLElement ;
1187+ const advancedToggle = container . querySelector (
1188+ "#advancedOptionsToggle"
1189+ ) as HTMLInputElement ;
1190+
1191+ if ( ! clientToken ) {
1192+ resultDiv . className =
1193+ "shared-result shared-result--visible shared-result--error" ;
1194+ resultDiv . innerHTML = `
1195+ <strong>Configuration Error</strong><br>
1196+ <small>Please add STORYBOOK_BRAINTREE_CLIENT_TOKEN to your .env file</small>
1197+ ` ;
1198+ return ;
1199+ }
1200+
1201+ try {
1202+ const braintree = getBraintreeSDK ( resultDiv ) ;
1203+ const clientInstance = await braintree . client . create ( {
1204+ authorization : clientToken ,
1205+ } ) ;
1206+
1207+ const paypalCheckoutV6Instance = await braintree . paypalCheckoutV6 . create ( {
1208+ client : clientInstance ,
1209+ } ) ;
1210+
1211+ await paypalCheckoutV6Instance . loadPayPalSDK ( ) ;
1212+
1213+ const getSessionOptions = ( ) => {
1214+ const baseOptions = advancedToggle . checked
1215+ ? CHECKOUT_WITH_VAULT_ADVANCED
1216+ : CHECKOUT_WITH_VAULT_BASIC ;
1217+
1218+ return {
1219+ ...baseOptions ,
1220+ onApprove : async ( data : IPayPalV6ApproveData ) => {
1221+ const tokenizeData = {
1222+ payerID : data . payerID || data . payerId || data . PayerID ,
1223+ orderID : getOrderId ( data ) ,
1224+ } ;
1225+
1226+ const payload =
1227+ await paypalCheckoutV6Instance . tokenizePayment ( tokenizeData ) ;
1228+
1229+ const email =
1230+ payload . details ?. email || payload . details ?. payerEmail || "N/A" ;
1231+ const mode = advancedToggle . checked
1232+ ? "with line items and shipping"
1233+ : "basic payment" ;
1234+
1235+ resultDiv . className =
1236+ "shared-result shared-result--visible shared-result--success" ;
1237+ resultDiv . innerHTML = `
1238+ <strong>Payment authorized & account vaulted!</strong><br>
1239+ <small>Nonce: ${ payload . nonce } </small><br>
1240+ <small>Payer Email: ${ email } </small><br>
1241+ <small>Amount: $${ advancedToggle . checked ? "20.00" : "10.00" } </small><br>
1242+ <small>Mode: ${ mode } </small>
1243+ ` ;
1244+ } ,
1245+
1246+ onCancel : ( ) => {
1247+ resultDiv . className = "shared-result shared-result--visible" ;
1248+ resultDiv . innerHTML = `
1249+ <strong>Payment Cancelled</strong><br>
1250+ <small>Customer cancelled the checkout with vault flow.</small>
1251+ ` ;
1252+ } ,
1253+
1254+ onError : ( err : IBraintreeError ) => {
1255+ showDetailedError ( resultDiv , "PayPal Error" , err ) ;
1256+ } ,
1257+ } ;
1258+ } ;
1259+
1260+ const paypalButtonContainer = container . querySelector (
1261+ "#paypal-button"
1262+ ) as HTMLElement ;
1263+ const button = document . createElement ( "button" ) ;
1264+ button . textContent = "Checkout with Vault" ;
1265+ button . className = "paypal-button" ;
1266+ button . style . cssText = `
1267+ background-color: #0070ba;
1268+ color: white;
1269+ border: none;
1270+ padding: 12px 24px;
1271+ font-size: 16px;
1272+ border-radius: 4px;
1273+ cursor: pointer;
1274+ font-weight: 500;
1275+ width: 100%;
1276+ ` ;
1277+
1278+ button . addEventListener ( "click" , ( ) => {
1279+ const session =
1280+ paypalCheckoutV6Instance . createCheckoutWithVaultSession (
1281+ getSessionOptions ( )
1282+ ) ;
1283+ session . start ( ) ;
1284+ } ) ;
1285+
1286+ paypalButtonContainer . appendChild ( button ) ;
1287+ } catch ( error ) {
1288+ showDetailedError (
1289+ resultDiv ,
1290+ "Initialization Error" ,
1291+ error as IBraintreeError
1292+ ) ;
1293+ }
1294+ } ;
1295+
1296+ export const CheckoutWithVault : StoryObj = {
1297+ render : createSimpleBraintreeStory (
1298+ async ( container ) => {
1299+ const formContainer = createCheckoutWithVaultForm ( ) ;
1300+ container . appendChild ( formContainer ) ;
1301+ await setupCheckoutWithVault ( formContainer ) ;
1302+ } ,
1303+ [ "client.min.js" , "paypal-checkout-v6.min.js" ]
1304+ ) ,
1305+ } ;
0 commit comments