11import type { Stripe } from '@stripe/stripe-js' ;
2- import type {
3- SubmissionData ,
4- SubmissionOptions ,
5- SubmissionBody ,
6- SubmissionResponse ,
7- } from './forms' ;
2+ import { Session } from './session' ;
3+ import {
4+ SubmissionError ,
5+ SubmissionSuccess ,
6+ StripeSCAPending ,
7+ isServerErrorResponse ,
8+ isServerSuccessResponse ,
9+ isServerStripeSCAPendingResponse ,
10+ type FieldValues ,
11+ type SubmissionData ,
12+ type SubmissionOptions ,
13+ type SubmissionResult ,
14+ } from './submission' ;
815import {
916 appendExtraData ,
1017 clientHeader ,
1118 encode64 ,
12- handleLegacyErrorPayload ,
13- handleSCA ,
19+ isUnknownObject ,
1420} from './utils' ;
15- import { Session } from './session' ;
1621
1722export interface Config {
1823 project ?: string ;
@@ -22,19 +27,13 @@ export interface Config {
2227export class Client {
2328 project : string | undefined ;
2429 stripePromise : Stripe | undefined ;
25- private session : Session | undefined ;
30+ private readonly session ? : Session ;
2631
2732 constructor ( config : Config = { } ) {
2833 this . project = config . project ;
2934 this . stripePromise = config . stripePromise ;
30- if ( typeof window !== 'undefined' ) this . startBrowserSession ( ) ;
31- }
3235
33- /**
34- * Starts a browser session.
35- */
36- startBrowserSession ( ) : void {
37- if ( ! this . session ) {
36+ if ( typeof window !== 'undefined' ) {
3837 this . session = new Session ( ) ;
3938 }
4039 }
@@ -46,22 +45,16 @@ export class Client {
4645 * @param data - An object or FormData instance containing submission data.
4746 * @param args - An object of form submission data.
4847 */
49- async submitForm (
48+ async submitForm < T extends FieldValues > (
5049 formKey : string ,
51- data : SubmissionData ,
50+ data : SubmissionData < T > ,
5251 opts : SubmissionOptions = { }
53- ) : Promise < SubmissionResponse > {
52+ ) : Promise < SubmissionResult < T > > {
5453 const endpoint = opts . endpoint || 'https://formspree.io' ;
55- const fetchImpl = opts . fetchImpl || fetch ;
5654 const url = this . project
5755 ? `${ endpoint } /p/${ this . project } /f/${ formKey } `
5856 : `${ endpoint } /f/${ formKey } ` ;
5957
60- const serializeBody = ( data : SubmissionData ) : FormData | string => {
61- if ( data instanceof FormData ) return data ;
62- return JSON . stringify ( data ) ;
63- } ;
64-
6558 const headers : { [ key : string ] : string } = {
6659 Accept : 'application/json' ,
6760 'Formspree-Client' : clientHeader ( opts . clientName ) ,
@@ -75,78 +68,123 @@ export class Client {
7568 headers [ 'Content-Type' ] = 'application/json' ;
7669 }
7770
78- const request = {
79- method : 'POST' ,
80- mode : 'cors' as const ,
81- body : serializeBody ( data ) ,
82- headers,
83- } ;
71+ async function makeFormspreeRequest (
72+ data : SubmissionData < T >
73+ ) : Promise < SubmissionResult < T > | StripeSCAPending > {
74+ try {
75+ const res = await fetch ( url , {
76+ method : 'POST' ,
77+ mode : 'cors' ,
78+ body : data instanceof FormData ? data : JSON . stringify ( data ) ,
79+ headers,
80+ } ) ;
81+
82+ const body = await res . json ( ) ;
83+
84+ if ( isUnknownObject ( body ) ) {
85+ if ( isServerErrorResponse ( body ) ) {
86+ return Array . isArray ( body . errors )
87+ ? new SubmissionError ( ...body . errors )
88+ : new SubmissionError ( { message : body . error } ) ;
89+ }
90+
91+ if ( isServerStripeSCAPendingResponse ( body ) ) {
92+ return new StripeSCAPending (
93+ body . stripe . paymentIntentClientSecret ,
94+ body . resubmitKey
95+ ) ;
96+ }
97+
98+ if ( isServerSuccessResponse ( body ) ) {
99+ return new SubmissionSuccess ( { next : body . next } ) ;
100+ }
101+ }
102+
103+ return new SubmissionError ( {
104+ message : 'Unexpected response format' ,
105+ } ) ;
106+ } catch ( err ) {
107+ const message =
108+ err instanceof Error
109+ ? err . message
110+ : `Unknown error while posting to Formspree: ${ JSON . stringify (
111+ err
112+ ) } `;
113+ return new SubmissionError ( { message : message } ) ;
114+ }
115+ }
84116
85- // first check if we need to add the stripe paymentMethod
86117 if ( this . stripePromise && opts . createPaymentMethod ) {
87- // Get Stripe payload
88- const payload = await opts . createPaymentMethod ( ) ;
89-
90- if ( payload . error ) {
91- // Return the error in case Stripe failed to create a payment method
92- return {
93- response : null ,
94- body : {
95- errors : [
96- {
97- code : 'STRIPE_CLIENT_ERROR' ,
98- message : 'Error creating payment method' ,
99- field : 'paymentMethod' ,
100- } ,
101- ] ,
102- } ,
103- } ;
118+ const createPaymentMethodResult = await opts . createPaymentMethod ( ) ;
119+
120+ if ( createPaymentMethodResult . error ) {
121+ return new SubmissionError ( {
122+ code : 'STRIPE_CLIENT_ERROR' ,
123+ field : 'paymentMethod' ,
124+ message : 'Error creating payment method' ,
125+ } ) ;
104126 }
105127
106128 // Add the paymentMethod to the data
107- appendExtraData ( data , 'paymentMethod' , payload . paymentMethod . id ) ;
129+ appendExtraData (
130+ data ,
131+ 'paymentMethod' ,
132+ createPaymentMethodResult . paymentMethod . id
133+ ) ;
108134
109135 // Send a request to Formspree server to handle the payment method
110- const response = await fetchImpl ( url , {
111- ...request ,
112- body : serializeBody ( data ) ,
113- } ) ;
114- const responseData = await response . json ( ) ;
115-
116- // Handle SCA
117- if (
118- responseData &&
119- responseData . stripe &&
120- responseData . stripe . requiresAction &&
121- responseData . resubmitKey
122- ) {
123- return await handleSCA ( {
124- stripePromise : this . stripePromise ,
125- responseData,
126- response,
127- payload,
128- data,
129- fetchImpl,
130- request,
131- url,
132- } ) ;
136+ const result = await makeFormspreeRequest ( data ) ;
137+
138+ if ( result . kind === 'error' ) {
139+ return result ;
133140 }
134141
135- return handleLegacyErrorPayload ( {
136- response,
137- body : responseData ,
138- } ) ;
139- } else {
140- return fetchImpl ( url , request )
141- . then ( ( response ) => {
142- return response
143- . json ( )
144- . then ( ( body : SubmissionBody ) : SubmissionResponse => {
145- return handleLegacyErrorPayload ( { body, response } ) ;
146- } ) ;
147- } )
148- . catch ( ) ;
142+ if ( result . kind === 'stripePluginPending' ) {
143+ const stripeResult = await this . stripePromise . handleCardAction (
144+ result . paymentIntentClientSecret
145+ ) ;
146+
147+ if ( stripeResult . error ) {
148+ return new SubmissionError ( {
149+ code : 'STRIPE_CLIENT_ERROR' ,
150+ field : 'paymentMethod' ,
151+ message : 'Stripe SCA error' ,
152+ } ) ;
153+ }
154+
155+ // `paymentMethod` must not be on the payload when resubmitting
156+ // the form to handle Stripe SCA.
157+ if ( data instanceof FormData ) {
158+ data . delete ( 'paymentMethod' ) ;
159+ } else {
160+ delete data . paymentMethod ;
161+ }
162+
163+ appendExtraData ( data , 'paymentIntent' , stripeResult . paymentIntent . id ) ;
164+ appendExtraData ( data , 'resubmitKey' , result . resubmitKey ) ;
165+
166+ // Resubmit the form with the paymentIntent and resubmitKey
167+ const resubmitResult = await makeFormspreeRequest ( data ) ;
168+ assertSubmissionResult ( resubmitResult ) ;
169+ return resubmitResult ;
170+ }
171+
172+ return result ;
149173 }
174+
175+ const result = await makeFormspreeRequest ( data ) ;
176+ assertSubmissionResult ( result ) ;
177+ return result ;
178+ }
179+ }
180+
181+ // assertSubmissionResult ensures the result is SubmissionResult
182+ function assertSubmissionResult < T extends FieldValues > (
183+ result : SubmissionResult < T > | StripeSCAPending
184+ ) : asserts result is SubmissionResult < T > {
185+ const { kind } = result ;
186+ if ( kind !== 'success' && kind !== 'error' ) {
187+ throw new Error ( `Unexpected submission result (kind: ${ kind } )` ) ;
150188 }
151189}
152190
0 commit comments