diff --git a/firestore-stripe-payments/README.md b/firestore-stripe-payments/README.md index babfff02..e9bf11af 100644 --- a/firestore-stripe-payments/README.md +++ b/firestore-stripe-payments/README.md @@ -60,6 +60,49 @@ Then, in the [Stripe Dashboard](https://dashboard.stripe.com): - Create a new [restricted key](https://stripe.com/docs/keys#limit-access) with write access for the "Customers", "Checkout Sessions" and "Customer portal" resources, and read-only access for the "Subscriptions" and "Prices" resources. +#### Installing via Firebase CLI + +When installing via the CLI, be sure to pin the version. + +``` +firebase ext:install invertase/firestore-stripe-payments --project=projectId_or_alias +Alternatively for local source: +firebase ext:install . --project=projectId_or_alias +``` + +The current version can be found in [extension.yaml](extension.yaml). + +#### Using webhooks locally + +If you wish to test the webhooks **locally**, use the following command to configure the extension: + +``` +firebase ext:configure firestore-stripe-payments --local +``` + +Be sure to configure your test mode [API Key](https://stripe.com/docs/keys) and webhook [signing secret](https://stripe.com/docs/webhooks/signatures#:~:text=Before%20you%20can%20verify%20signatures,secret%20key%20for%20each%20endpoint.) when prompted. + +Start the firebase emulator with: + +``` +firebase emulators:start --project=projectId_or_alias +``` + +Find the functions path associated with the stripe extension, typically it looks like this: + +- `http://192.0.0.1:5001/{projectId}/{region}/ext-firestore-stripe-payments-handleWebhookEvents` + +- You can tunnel your local endpoint using a tool like [ngrok](https://ngrok.com/). In this case you will tunnel the localhost domain and port `http://127.0.01:5001`. Replace `127.0.0.1:5001` with your tunnel url. The end result would look something like: + +``` +https://1234-1234-1234.ngrok.io/{projectId}/{region}/ext-firestore-stripe-payments-handleWebhookEvents +``` + +- Configure your test mode stripe [webhook endpoint](https://stripe.com/docs/webhooks) with the url you just constructed. + +- Your local webhooks are now set up. + + #### Billing This extension uses the following Firebase services which may have associated charges: diff --git a/firestore-stripe-payments/functions/__tests__/tests/checkoutsessions/createCheckoutSession.test.ts b/firestore-stripe-payments/functions/__tests__/tests/checkoutsessions/createCheckoutSession.test.ts index 6a928b4a..546406da 100644 --- a/firestore-stripe-payments/functions/__tests__/tests/checkoutsessions/createCheckoutSession.test.ts +++ b/firestore-stripe-payments/functions/__tests__/tests/checkoutsessions/createCheckoutSession.test.ts @@ -66,6 +66,52 @@ describe('createCheckoutSession', () => { expect(success_url).toBe('http://test.com/success'); }); + test('should setup a future payment with mode:setup', async () => { + const collection = firestore.collection('customers'); + + const customer: DocumentData = await waitForDocumentToExistInCollection( + collection, + 'email', + user.email + ); + + /** Define params */ + const client = 'web'; + const mode = 'setup'; + const success_url = 'http://test.com/success'; + const cancel_url = 'http://test.com/cancel'; + const payment_method_types = ['card']; + + const checkoutSessionCollection = collection + .doc(customer.doc.id) + .collection('checkout_sessions'); + + const checkoutSessionDocument: DocumentReference = + await checkoutSessionCollection.add({ + client: 'web', + mode: 'setup', + success_url: 'http://test.com/success', + cancel_url: 'http://test.com/cancel', + payment_method_types, + }); + + const customerDoc = await waitForDocumentToExistWithField( + checkoutSessionDocument, + 'created' + ); + + const result = customerDoc.data(); + + expect(result.client).toBe(client); + expect(result.mode).toBe(mode); + expect(result.success_url).toBe(success_url); + expect(result.cancel_url).toBe(cancel_url); + expect(result.payment_method_types).toEqual(payment_method_types); + expect(result.sessionId).toBeDefined(); + expect(url).toBeDefined(); + expect(result.created).toBeDefined(); + }); + test.skip('throws an error when success_url has not been provided', async () => {}); test.skip('throws an error when cancel_url has not been provided', async () => {}); diff --git a/firestore-stripe-payments/functions/src/index.ts b/firestore-stripe-payments/functions/src/index.ts index ed5e451b..2cb5052b 100644 --- a/firestore-stripe-payments/functions/src/index.ts +++ b/firestore-stripe-payments/functions/src/index.ts @@ -104,6 +104,26 @@ exports.createCustomer = functions.auth }); }); +function generateLineItems( + line_items: Stripe.Checkout.SessionCreateParams.LineItem[], + price: string, + quantity: number +): Stripe.Checkout.SessionCreateParams.LineItem[] { + if (line_items) { + return line_items; + } + + if (price && quantity) + return [ + { + price, + quantity, + }, + ]; + + return []; +} + /** * Create a CheckoutSession or PaymentIntent based on which client is being used. */ @@ -185,14 +205,7 @@ exports.createCheckoutSession = functions shipping_rates, customer, customer_update, - line_items: line_items - ? line_items - : [ - { - price, - quantity, - }, - ], + line_items: generateLineItems(line_items, price, quantity), mode, success_url, cancel_url, @@ -205,6 +218,7 @@ exports.createCheckoutSession = functions if (payment_method_types) { sessionCreateParams.payment_method_types = payment_method_types; } + if (mode === 'subscription') { sessionCreateParams.payment_method_collection = payment_method_collection; @@ -244,15 +258,17 @@ exports.createCheckoutSession = functions } if (promotion_code) { sessionCreateParams.discounts = [{ promotion_code }]; - } else { + } else if (mode !== 'setup') { sessionCreateParams.allow_promotion_codes = allow_promotion_codes; } if (client_reference_id) sessionCreateParams.client_reference_id = client_reference_id; + const session = await stripe.checkout.sessions.create( sessionCreateParams, { idempotencyKey: context.params.id } ); + await snap.ref.set( { client,