@flopay/react 1.10.0 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/chunk-JUUKXJQQ.mjs +1 -0
- package/dist/chunk-YSFYLEHB.mjs +1 -0
- package/dist/index.cjs +9 -9
- package/dist/index.mjs +7 -7
- package/dist/payment-method-setup-entry.cjs +2 -2
- package/dist/payment-method-setup-entry.mjs +1 -1
- package/dist/payment-method-setup-paypal-FRRQW56L.mjs +1 -0
- package/package.json +3 -3
- package/dist/chunk-R73QOXY5.mjs +0 -1
- package/dist/chunk-U3PMJDD4.mjs +0 -1
- package/dist/payment-method-setup-paypal-MWDJVYVO.mjs +0 -1
package/README.md
CHANGED
|
@@ -203,7 +203,9 @@ It drops only complete exception stacks with no app or `@flopay/*` frame and at
|
|
|
203
203
|
|
|
204
204
|
`FloPayPaymentMethodSetup` lets a customer save any payment method the setup session offers, not only a card, without a purchase or charge. It takes the same `sessionId` and `nonce` as `FloPayCardSetup`, and the same callbacks, `theme` and `containerStyle`.
|
|
205
205
|
|
|
206
|
-
- **The session decides what renders.** The component renders exactly the methods the setup session
|
|
206
|
+
- **The session decides what renders.** The component renders exactly the methods the setup session lists in `advertisedPaymentMethods`, and nothing else. It never reads the purchase-checkout `gateways` block, which lists nothing on a setup session. A `card` entry mounts the hosted card form. A `wallet` or `apm` entry from Stripe renders that method with Stripe Elements in setup mode, using that entry's own `publishableKey`. A `paypal` entry (`paypal_vaulted`) renders the PayPal vault flow, with the entry's key as the client id and its `environment`. An unknown family or type is skipped without an error, and the first entry for a method wins. A session without the field (an older backend) renders the card form only, exactly like `FloPayCardSetup`. An empty list reports `NoAdvertisedPaymentMethods`. The SDK does no gateway selection, filtering or inference. Each rendered method's wrapper carries `data-flopay-payment-method="<method>"`.
|
|
207
|
+
- **Stripe methods use the session's own currency.** Stripe wallets and alternative methods receive the `currency` the setup-session read carries, never a guessed one. When the read carries none, they are not rendered; the card form and PayPal are unaffected. If a session advertises only Stripe wallets or alternative methods and the read carries no currency, nothing can render: `onError` receives `NoAdvertisedPaymentMethods` (`retryable: false`) with a message that names those methods and the missing currency.
|
|
208
|
+
- **A failing method never takes the card form down.** When a wallet, alternative method or PayPal fails for good (its code does not load, it throws, or the provider reports a failure that cannot be retried), that method is removed, the others stay mounted, and `onError` receives the failure with `retryable: true` while another advertised method is still usable. Keep the setup open on a retryable error. Only when nothing usable is left is it `retryable: false`. A failure the method itself reports as retryable is passed through unchanged, and the method stays.
|
|
207
209
|
- **Card-only sessions behave exactly like `FloPayCardSetup`.** They use the same hosted form, overlay wording (`SAVING CARD...`, `CARD SAVED`, `CARD NOT SAVED`) and callbacks, and never request Stripe.js. Stripe.js loads lazily, only when the session advertises a Stripe wallet or alternative method. A PayPal-only session never loads Stripe either.
|
|
208
210
|
- **Success is the backend's call.** `onComplete` fires once, only after the billing API reports the setup verified. For a wallet, alternative method or PayPal, provider approval alone is not success: the component re-reads the session until it completes. A decline, cancel or failure leaves previously saved methods unchanged; the component never lists, replaces or deletes saved methods.
|
|
209
211
|
- **Redirect methods return to the same page.** When the buyer comes back from a bank or wallet redirect (Stripe's `setup_intent` and `redirect_status` query parameters), the component re-reads the session. A completed session reports `onComplete`, and `redirect_status=failed` reports `onDecline`. Anything else waits for verification. The query parameters are removed once read. The hosted card path never posts `/3ds/complete`.
|
|
@@ -712,10 +714,10 @@ A `validation_error` is also reported when both `showStripe` and `showPayPal` ar
|
|
|
712
714
|
| Code | When |
|
|
713
715
|
|------|------|
|
|
714
716
|
| `InvalidPaymentMethodSetupSession` | The session is not a zero-amount, product-free setup session |
|
|
715
|
-
| `NoAdvertisedPaymentMethods` | The setup session advertises no payment method |
|
|
717
|
+
| `NoAdvertisedPaymentMethods` | The setup session advertises no payment method, or only Stripe wallets or alternative methods while the read carries no currency (the message names them) |
|
|
716
718
|
| `payment_method_setup_unverified` | The provider accepted the method but the billing API had not verified it in time (retryable) |
|
|
717
719
|
| `payment_method_setup_expired` | The setup session expired before the method was verified |
|
|
718
720
|
| `payment_method_setup_failed` | A wallet or alternative method failed technically (retryable) |
|
|
719
|
-
| `payment_method_setup_load_failed` | The code for a wallet, alternative method or PayPal could not be loaded
|
|
721
|
+
| `payment_method_setup_load_failed` | The code for a wallet, alternative method or PayPal could not be loaded, or it failed while rendering. That method is removed and the other advertised methods remain usable, so it is `retryable: true` while one is left, and `retryable: false` only when it was the last usable method. Only a page reload can load that method again |
|
|
720
722
|
| `paypal_setup_failed` | The PayPal vault flow failed technically (retryable) |
|
|
721
723
|
| `popup_blocked` | PayPal could not open its approval window; allow pop-ups and retry (retryable) |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as we,b as Me,c as v,d as be,f as _e,i as Ee,k}from"./chunk-I4TPZWBS.mjs";import{PaymentAPI as De,PciVaultCardCapture as Je}from"@flopay/js/card-setup";import{FloPayError as M,isCardSetupCheckoutSession as Qe,resolveBillingApiUrl as ee,resolveTheme as Xe}from"@flopay/shared";import Ze,{Suspense as Le,useCallback as D,useEffect as te,useMemo as L,useRef as f,useState as w}from"react";import{PAYPAL_VAULTED_PAYMENT_METHOD_TYPE as Ge}from"@flopay/shared";var He=new Set(["apple_pay","google_pay","link","paypal","amazon_pay"]),je=new Set(["wallet","apm"]);function T(t){return typeof t=="string"&&t.trim().length>0}function Ke(t){if(typeof t!="object"||t===null)return!1;let o=t;return T(o.family)&&T(o.paymentMethodType)}function ze(t){let n=t.raw?.data?.currency;return T(n)?n.trim().toUpperCase():null}function Re(t){let o=ze(t),n=t.data.session?.advertisedPaymentMethods;if(!Array.isArray(n))return{card:!0,stripe:[],paypal:null,currency:o,skippedForMissingCurrency:[]};let s={card:!1,stripe:[],paypal:null,currency:o,skippedForMissingCurrency:[]},l=new Set;for(let i of n){if(!Ke(i)||l.has(i.paymentMethodType))continue;let{family:p,paymentMethodType:m,providerType:I,publishableKey:b}=i;if(p==="card"){if(m!=="card")continue;s.card=!0}else if(je.has(p)){if(I!=="stripe"||m==="card"||!T(b))continue;if(o===null){s.skippedForMissingCurrency.includes(m)||s.skippedForMissingCurrency.push(m);continue}s.stripe.push({method:m,publishableKey:b,currency:o})}else if(p==="paypal"){if(m!==Ge||I!=="paypal"||!T(b))continue;s.paypal={clientId:b,...T(i.environment)?{environment:i.environment}:{}}}else continue;l.add(m)}return s.paypal&&(s.stripe=s.stripe.filter(i=>i.method!=="paypal")),s}function Z(t){return[...t.card?["card"]:[],...t.stripe.map(o=>o.method),...t.paypal?["paypal"]:[]]}function Fe(t){return Z(t).length>0}function Ae(t){return He.has(t)?"wallet":"apm"}var qe=["setup_intent","setup_intent_client_secret","redirect_status"];function ke(){if(typeof window>"u")return null;let t=new URLSearchParams(window.location.search);return!t.has("setup_intent")||!t.has("redirect_status")?null:{failed:t.get("redirect_status")==="failed"}}function Te(){if(typeof window>"u")return;let t=new URL(window.location.href),o=!1;for(let n of qe)t.searchParams.has(n)&&(t.searchParams.delete(n),o=!0);o&&window.history.replaceState(window.history.state,"",t.toString())}function Ve(t){return t==="card"?{processingLabel:"SAVING CARD...",successLabel:"CARD SAVED",errorLabel:"CARD NOT SAVED"}:{processingLabel:"SAVING PAYMENT METHOD...",successLabel:"PAYMENT METHOD SAVED",errorLabel:"PAYMENT METHOD NOT SAVED"}}import{Fragment as Ie,jsx as h,jsxs as xe}from"react/jsx-runtime";var et=15,tt=2e3;function rt(t){return{userId:t.customer?.id??"",email:t.customer?.email??""}}function re(t){return new Promise(o=>setTimeout(o,t))}async function nt(t,o,n){if(!o.trim())throw new M("FloPayPaymentMethodSetup requires a sessionId.","validation_error",{code:"MissingCheckoutSessionId",param:"sessionId"});if(!n.trim())throw new M("FloPayPaymentMethodSetup requires a session nonce.","validation_error",{code:"MissingCheckoutSessionToken",param:"nonce"});let s=await t.getUnifiedCheckoutSession(o,n),l=s.data.session;if(!l||!Qe(l))throw new M("FloPayPaymentMethodSetup only accepts a zero-amount, productless setup session.","validation_error",{code:"InvalidPaymentMethodSetupSession",param:"sessionId"});let i=Re(s);if(!Fe(i)){let{skippedForMissingCurrency:p}=i,m=p.length>0?`FloPay: this setup session advertises only Stripe methods (${p.join(", ")}), and they need the session's currency, which the setup-session read did not carry.`:"FloPay: this setup session advertises no payment method.";throw new M(m,"validation_error",{code:"NoAdvertisedPaymentMethods",param:"sessionId"})}return{session:l,advertisement:i}}async function at(t,o,n,s){let l=o.vault??null;if(l?.html?.trim()||(l=await t.getVaultCapture(n,s)),!l.html?.trim())throw new M("FloPay: this setup session has no vault card capability. Upgrade the billing backend integration.","api_error",{code:"UnsupportedBackendVaultCapability"});return l}var j=class extends Ze.Component{constructor(){super(...arguments);this.state={failed:!1}}static getDerivedStateFromError(){return{failed:!0}}componentDidCatch(n){this.props.onFailed(n,{code:"payment_method_setup_load_failed",retryable:!1})}render(){return this.state.failed?null:this.props.children}};function gt({branches:{StripeSetupMethod:t,PayPalSetupMethod:o},sessionId:n,nonce:s,billingApiUrl:l,telemetry:i,theme:p="modern-light",containerStyle:m,loading:I=null,onReady:b,onComplete:ne,onDecline:ae,onCancel:oe,onValidation:se,onError:ie}){let[K]=w(ke),[x,z]=w(null),[Be,q]=w(!1),[P,le]=w(null),[B,ue]=w(null),[ce,de]=w(null),[N,y]=w(null),[$,pe]=w(()=>new Set),U=f($),me=f(null);me.current=x;let g=f(!1),J=f(!1),V=f(!1),Q=f(!1),X=f(0),Y=f(0),E=f(!1),S=L(()=>typeof p=="object"?void 0:Xe(p),[p]),Ne=L(()=>typeof p=="object"?p:S?Ee({appearance:S.appearance,buttonsStyles:S.buttonsLayout,layout:"default"}):null,[S,p]),Ue=L(()=>we(S?.buttonsLayout.cardFormContainer,m),[S,m]),ye=L(()=>S?Me(S):void 0,[S]),d=f({sessionId:n,nonce:s,billingApiUrl:l,telemetry:i,onReady:b,onComplete:ne,onDecline:ae,onCancel:oe,onValidation:se,onError:ie});d.current={sessionId:n,nonce:s,billingApiUrl:l,telemetry:i,onReady:b,onComplete:ne,onDecline:ae,onCancel:oe,onValidation:se,onError:ie},te(()=>{E.current=!0;let e=X.current+1;return X.current=e,()=>{E.current=!1,Y.current+=1,queueMicrotask(()=>{X.current===e&&(V.current||Q.current||(Q.current=!0,v(()=>{d.current.onCancel?.({status:"cancelled",sessionId:n})})))})}},[n]);let R=D((e,r)=>{V.current=!0,g.current=!1,y({status:"success",wording:e,message:null}),(async()=>(await re(600),!J.current&&(J.current=!0,v(()=>{d.current.onComplete?.({status:"succeeded",sessionId:d.current.sessionId,...r?{paymentMethod:r}:{}})}),E.current&&y(null))))()},[]),F=D((e,r)=>{V.current=!0,g.current=!1,y({status:"error",wording:e,message:r.message??null}),v(()=>{d.current.onDecline?.({status:"declined",sessionId:d.current.sessionId,...r.reason?{reason:r.reason}:{},...r.message?{message:r.message}:{}})}),(async()=>(await re(400),E.current&&y(null)))()},[]),A=D((e,r)=>{g.current?(g.current=!1,y({status:"error",wording:e,message:r.message}),setTimeout(()=>{E.current&&y(null)},400)):y(null),v(()=>d.current.onError?.(r))},[]),W=D(async e=>{let r=Y.current+1;Y.current=r;let u=()=>!E.current||r!==Y.current;g.current=!0,y({status:"processing",wording:"payment_method",message:null});let c=new De(ee(d.current.billingApiUrl),{telemetry:d.current.telemetry});try{for(let a=0;a<et;a+=1){if((a>0||e.waitFirst)&&await re(tt),u())return;let C=await c.getUnifiedCheckoutSession(d.current.sessionId,d.current.nonce);if(u())return;let _=C.data.session?.status;if(_==="complete"){q(!0),R("payment_method");return}if(_==="expired")throw new M("This setup session expired before the payment method was saved.","validation_error",{code:"payment_method_setup_expired"})}throw new M("The payment method could not be confirmed yet. Please try again.","api_error",{code:"payment_method_setup_unverified"})}catch(a){if(u())return;let C=a instanceof M&&a.code==="payment_method_setup_unverified";A("payment_method",k(a,C?{retryable:!0}:{}))}finally{c.destroy()}},[A,R]);te(()=>{let e=!0,r=new De(ee(l),{telemetry:i});z(null),q(!1),le(null),ue(null),de(null);let u=new Set;return U.current=u,pe(u),V.current=!1,Q.current=!1,J.current=!1,(async()=>{try{let{session:c,advertisement:a}=await nt(r,n,s);if(!e)return;let C={advertisement:a,customer:rt(c)};if(K){if(Te(),c.status==="complete"){q(!0),z(C),R("payment_method");return}K.failed?F("payment_method",{reason:"redirect_failed"}):W({waitFirst:!0})}let _=a.card?await at(r,c,n,s):null;if(!e)return;z(C),_&&(ue(_),le(new Je({sessionId:n,operation:"card_setup",telemetry:i})))}catch(c){if(!e)return;let a=k(c);V.current=!0,de(a),v(()=>d.current.onError?.(a))}})(),()=>{e=!1,r.destroy()}},[l,s,K,F,R,n,i,W]),te(()=>{if(!P)return;g.current=!1;let e=P.on("submitting",()=>{g.current=!0,y({status:"processing",wording:"card",message:null})}),r=P.on("complete",a=>{R("card",a.paymentMethod)}),u=P.on("decline",a=>{F("card",{...a.declineReason?{reason:a.declineReason}:{},...a.message?{message:a.message}:{}})}),c=P.on("challenge_dismissed",()=>{y(null)});return()=>{e(),r(),u(),c()}},[P,F,R]);let fe=D((e,r,u)=>{if(u.retryable){A("payment_method",k(r,u));return}if(U.current.has(e))return;let c=new Set(U.current);c.add(e),U.current=c,pe(c);let a=me.current?.advertisement,C=a?Z(a).some(_=>!c.has(_)):!1;A("payment_method",k(r,{code:u.code,retryable:C}))},[A]),he=L(()=>({onSubmitting:()=>{g.current=!0,y({status:"processing",wording:"payment_method",message:null})},onConfirmed:()=>{W({waitFirst:!1})},onDeclined:e=>F("payment_method",e),onValidation:e=>{v(()=>d.current.onValidation?.(e))},onAbandoned:()=>{g.current=!1,y(null)}}),[F,W]),Se=f(fe);Se.current=fe;let ge=f(new Map),G=e=>{let r=ge.current.get(e);if(r)return r;let u=(c,a)=>Se.current(e,c,a);return ge.current.set(e,u),u},ve=N?h(be,{status:N.status,errorMessage:N.message,...Ve(N.wording),successNote:null,...ye?{theme:ye}:{}}):null;if(ce)return h("div",{role:"alert",children:ce.message});if(!x||Be)return xe(Ie,{children:[ve,x?null:I]});let{advertisement:O,customer:Ye}=x,Pe=ee(l),Ce=i===void 0?{}:{telemetry:i},We=O.stripe.filter(({method:e})=>!$.has(e)),H=$.has("paypal")?null:O.paypal;return xe(Ie,{children:[ve,O.card&&P&&B?.html?h("div",{"data-flopay-payment-method":"card",children:h(_e,{capture:P,html:B.html,messageToken:B.messageToken,expectedOrigin:B.expectedOrigin,theme:Ne,containerStyle:Ue,onReady:()=>{v(()=>d.current.onReady?.())},onValidation:e=>{v(()=>d.current.onValidation?.(e))},onError:e=>{e&&A("card",k(new Error(e),{code:"card_setup_widget_failed",retryable:!0}))}})}):null,We.map(({method:e,publishableKey:r,currency:u})=>h("div",{"data-flopay-payment-method":e,children:h(j,{onFailed:G(e),children:h(Le,{fallback:null,children:h(t,{method:e,category:Ae(e),publishableKey:r,currency:u,sessionId:n,nonce:s,billingApiUrl:Pe,...Ce,...S?{appearance:S.appearance}:{},...he,onFailed:G(e)})})})},e)),H?h("div",{"data-flopay-payment-method":"paypal",children:h(j,{onFailed:G("paypal"),children:h(Le,{fallback:null,children:h(o,{clientId:H.clientId,...H.environment?{environment:H.environment}:{},...O.currency?{currency:O.currency}:{},sessionId:n,nonce:s,billingApiUrl:Pe,customer:Ye,...Ce,...he,onFailed:G("paypal")})})})}):null]})}export{gt as a};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as T,g as A,i as R}from"./chunk-M7R46EDF.mjs";import{PaymentAPI as I}from"@flopay/js/card-setup";import{FloPayError as p,PAYPAL_VAULTED_PAYMENT_METHOD_TYPE as B}from"@flopay/shared";import{loadScript as x}from"@paypal/paypal-js";import{useEffect as D,useRef as C,useState as O}from"react";import{jsx as F}from"react/jsx-runtime";function z({clientId:d,environment:o,currency:s,sessionId:i,nonce:l,billingApiUrl:y,telemetry:f,customer:{userId:m,email:P},onSubmitting:h,onConfirmed:_,onFailed:v,onAbandoned:b}){let w=C(null),t=C({onSubmitting:h,onConfirmed:_,onFailed:v,onAbandoned:b});t.current={onSubmitting:h,onConfirmed:_,onFailed:v,onAbandoned:b};let[c]=O(()=>R().inAppBrowser);return D(()=>{let a=!0,k,u=new I(y,{telemetry:f}),g={userId:m,email:P};return(async()=>{try{let n=await x({clientId:d,...s?{currency:s.toUpperCase()}:{},components:"buttons",...o==="sandbox"||o==="production"?{environment:o}:{}}),M=w.current;if(!a||!M)return;if(!n?.Buttons)throw new p("PayPal could not be loaded.","api_error",{code:"paypal_unavailable"});let E=n.Buttons({createVaultSetupToken:async()=>{t.current.onSubmitting();let e=await u.createSessionIntent(i,l,{provider:"paypal",paymentMethodCategory:"wallet",paymentMethodType:B,paymentMethodId:null,intentKind:"setup"});if(e.provider!=="paypal"||e.providerObjectType!=="setup_token")throw new p("The billing API did not return a PayPal setup token.","api_error",{code:"InvalidSessionIntentResponse"});return e.providerObjectId},onApprove:async e=>{let r=e.vaultSetupToken;if(!r)throw new p("PayPal approved without a setup token.","api_error",{code:"paypal_setup_token_missing"});let S=await u.processPayment(g.userId,{sessionId:i,nonce:l,paymentMethodCategory:"paypal",tokenizedData:{id:r,type:"paypal_setup_token",isPaypal:!0,gateway:"paypal",paymentMethodType:B},accountData:g});if(!S.ok)throw new p("PayPal could not be saved.","api_error",{code:"paypal_setup_failed",statusCode:S.status});a&&t.current.onConfirmed()},onCancel:()=>{a&&t.current.onAbandoned()},onError:e=>{if(!a)return;let{popupBlocked:r}=A(e,{inAppBrowser:c,handoffObserved:!1});t.current.onFailed(r?new Error(T("PayPal",!0,c)):e,{code:r?"popup_blocked":"paypal_setup_failed",retryable:!0})}});k=()=>E.close(),await E.render(M)}catch(n){if(!a)return;t.current.onFailed(n,{code:"paypal_unavailable",retryable:!0})}})(),()=>{a=!1,k?.().catch(()=>{}),u.destroy()}},[y,d,s,P,o,c,l,i,f,m]),F("div",{ref:w,"data-flopay-paypal-setup":""})}export{z as a};
|