diff --git a/firestore-stripe-payments/CHANGELOG.md b/firestore-stripe-payments/CHANGELOG.md index cd304fa6..153d76cd 100644 --- a/firestore-stripe-payments/CHANGELOG.md +++ b/firestore-stripe-payments/CHANGELOG.md @@ -37,6 +37,7 @@ [feat] - Add `setup_future_usage` parameter for one-time payment flows ## 0.3.5 - 2023-08-14 + [chore] Updated naming and upgraded to node18 [fix] Updated icons @@ -44,18 +45,23 @@ [fix] updated appinfo versioning ## 0.3.4 - 2023-08-14 + This extension has been formally transferred to Invertase. See the updated README for more details. ## Version 0.3.3 - 2023-03-20 + [fix] Address issue where `insertInvoiceRecord` function fails. [#511] ## Version 0.3.2 - 2022-11-30 + [chore] Added support for `us-west1` as a deployable region for Firebase functions. [#464] ## Version 0.3.1 - 2022-08-24 + [chore] Added `package-lock.json` to version control to prevent installation issues. [#426] ## Version 0.3.0 - 2022-08-23 + [feat] Allow configurable minimum instances for `createCheckoutSession` function. [#375] [feat] Throw an `unauthenticated` Firebase error from `creatPortalLink` function. [#420] @@ -63,12 +69,15 @@ This extension has been formally transferred to Invertase. See the updated READM [feat] Add Price object IDs from invoice line items for subscription payments to payments Firestore collection. [#393] ## Version 0.2.7 - 2022-05-10 + [chore] Updated package-lock.json that was causing install errors. ## Version 0.2.6 - 2022-05-10 + [feat] Added Stripe extension events. This extension emits events, which allows you to listen to and run custom logic at different trigger points during the functioning of the extension. For example you can listen to events when a product has been added via the `product.created` event, or whenever a payment has succeeded through the `invoice.payment_succeeded` event. (#386) ## Version 0.2.5 - 2022-04-20 + [feat] Add parameter to enable phone number collection. (#371) [feat] Add parameters to enable expired Checkout Session recovery. (#350) @@ -334,7 +343,7 @@ db.collection("products") Previously, only subscriptions created via Stripe Checkout were synced to Cloud Firestore. By additionally listening to the `customer.subscription.created` event, the extension now also captures subscriptions created via the Stripe Dashboard or directly via the API. For this to work, Firebase Authentication users need to be synced with Stripe customer objects and the customers collection in Cloud Firestore (new configuration added in version `0.1.7`). - - Add snippet on importing Stripe.js as an ES module when using a build toolchain for your client application (e.g. Angular, React, TypeScript, etc.) to `POSTINSTALL.md`. (#74) +- Add snippet on importing Stripe.js as an ES module when using a build toolchain for your client application (e.g. Angular, React, TypeScript, etc.) to `POSTINSTALL.md`. (#74) ## Version 0.1.6 - 2020-09-10 diff --git a/firestore-stripe-payments/POSTINSTALL.md b/firestore-stripe-payments/POSTINSTALL.md index 0a301a51..186e0387 100644 --- a/firestore-stripe-payments/POSTINSTALL.md +++ b/firestore-stripe-payments/POSTINSTALL.md @@ -175,15 +175,15 @@ The quickest way to sign-up new users is by using the [FirebaseUI library](https Products and pricing information are normal collections and docs in your Cloud Firestore and can be queried as such: ```js -db.collection('${param:PRODUCTS_COLLECTION}') - .where('active', '==', true) +db.collection("${param:PRODUCTS_COLLECTION}") + .where("active", "==", true) .get() .then(function (querySnapshot) { querySnapshot.forEach(async function (doc) { - console.log(doc.id, ' => ', doc.data()); - const priceSnap = await doc.ref.collection('prices').get(); + console.log(doc.id, " => ", doc.data()); + const priceSnap = await doc.ref.collection("prices").get(); priceSnap.docs.forEach((doc) => { - console.log(doc.id, ' => ', doc.data()); + console.log(doc.id, " => ", doc.data()); }); }); }); @@ -197,7 +197,7 @@ To create a Checkout Session ID for a one-time payment, pass `mode: 'payment` to ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) .collection("checkout_sessions") .add({ @@ -238,11 +238,11 @@ To subscribe the user to a specific pricing plan, create a new doc in the `check ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) - .collection('checkout_sessions') + .collection("checkout_sessions") .add({ - price: 'price_1GqIC8HYgolSBA35zoTTN2Zl', + price: "price_1GqIC8HYgolSBA35zoTTN2Zl", success_url: window.location.origin, cancel_url: window.location.origin, }); @@ -263,7 +263,7 @@ docRef.onSnapshot((snap) => { #### Handling trials -By default, the trial period days that you've specified on the pricing plan will be applied to the checkout session. Should you wish to not offer the trial for a certain user (e.g. they've previously had a subscription with a trial that they canceled and are now signing up again), you can specify `trial_from_plan: false` when creating the checkout session doc: +You can specify subscription trial period when creating the checkout session by using the `trial_period_days` parameter. Refer to the [docs](https://stripe.com/docs/payments/checkout/free-trials) for a detailed guide on free trials and how to set them up. ```js const docRef = await db @@ -272,7 +272,7 @@ const docRef = await db .collection("checkout_sessions") .add({ price: "price_1GqIC8HYgolSBA35zoTTN2Zl", - trial_from_plan: false, + trial_period_days: 7, success_url: window.location.origin, cancel_url: window.location.origin, }); @@ -286,11 +286,11 @@ In order for the promotion code redemption box to show up on the checkout page, ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser) - .collection('checkout_sessions') + .collection("checkout_sessions") .add({ - price: 'price_1GqIC8HYgolSBA35zoTTN2Zl', + price: "price_1GqIC8HYgolSBA35zoTTN2Zl", allow_promotion_codes: true, success_url: window.location.origin, cancel_url: window.location.origin, @@ -305,7 +305,7 @@ You can set a [promotion code](https://stripe.com/docs/billing/subscriptions/dis ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) .collection("checkout_sessions") .add({ @@ -326,7 +326,7 @@ Stripe Tax lets you calculate and collect sales tax, VAT, and GST. Know where to ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) .collection("checkout_sessions") .add({ @@ -340,11 +340,11 @@ const docRef = await db #### Applying tax rates dynamically -Stripe Checkout supports applying the correct tax rate for customers in US, GB, AU, and all countries in the EU. With [dynamic tax rates](https://stripe.com/docs/billing/subscriptions/taxes#adding-tax-rates-to-checkout), you create tax rates for different regions (e.g., a 20% VAT tax rate for customers in the UK and a 7.25% sales tax rate for customers in California, US) and Stripe attempts to match your customer’s location to one of those tax rates. +Stripe Checkout supports applying the correct tax rate for customers in US, GB, AU, and all countries in the EU. With [dynamic tax rates](https://stripe.com/docs/billing/subscriptions/taxes#adding-tax-rates-to-checkout), you create tax rates for different regions (e.g., a 20% VAT tax rate for customers in the UK and a 7.25% sales tax rate for customers in California, US) and Stripe attempts to match your customer’s location to one of those tax rates. ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser) .collection("checkout_sessions") .add({ @@ -352,7 +352,11 @@ const docRef = await db { price: "price_1HCUD4HYgolSBA35icTHEXd5", quantity: 1, - dynamic_tax_rates: ["txr_1IJJtvHYgolSBA35ITTBOaew", "txr_1Hlsk0HYgolSBA35rlraUVWO", "txr_1HCshzHYgolSBA35WkPjzOOi"], + dynamic_tax_rates: [ + "txr_1IJJtvHYgolSBA35ITTBOaew", + "txr_1Hlsk0HYgolSBA35rlraUVWO", + "txr_1HCshzHYgolSBA35WkPjzOOi", + ], }, ], success_url: window.location.origin, @@ -366,12 +370,12 @@ You can collect and report taxes with [Tax Rates](https://stripe.com/docs/billin ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser) - .collection('checkout_sessions') + .collection("checkout_sessions") .add({ - price: 'price_1GqIC8HYgolSBA35zoTTN2Zl', - tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'], + price: "price_1GqIC8HYgolSBA35zoTTN2Zl", + tax_rates: ["txr_1HCjzTHYgolSBA35m0e1tJN5"], success_url: window.location.origin, cancel_url: window.location.origin, }); @@ -385,7 +389,7 @@ Secondly, you need to add `collect_shipping_address: true` to the Checkout Sessi ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) .collection("checkout_sessions") .add({ @@ -402,15 +406,15 @@ You can optionally set a metadata object with key-value pairs when creating the ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') + .collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser) - .collection('checkout_sessions') + .collection("checkout_sessions") .add({ - price: 'price_1GqIC8HYgolSBA35zoTTN2Zl', + price: "price_1GqIC8HYgolSBA35zoTTN2Zl", success_url: window.location.origin, cancel_url: window.location.origin, metadata: { - item: 'item001', + item: "item001", }, }); ``` @@ -421,25 +425,25 @@ In addition to recurring prices, you can add one-time prices. These will only be ```js const docRef = await db - .collection('${param:CUSTOMERS_COLLECTION}') - .doc(currentUser) - .collection('checkout_sessions') - .add({ - line_items: [ - { - price: 'price_1HCUD4HYgolSBA35icTHEXd5', // RECURRING_PRICE_ID - quantity: 1, - tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'], - }, - { - price: 'price_1HEtgDHYgolSBA35LMkO3ExX', // ONE_TIME_PRICE_ID - quantity: 1, - tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'], - }, - ], - success_url: window.location.origin, - cancel_url: window.location.origin, - }); + .collection("${param:CUSTOMERS_COLLECTION}") + .doc(currentUser) + .collection("checkout_sessions") + .add({ + line_items: [ + { + price: "price_1HCUD4HYgolSBA35icTHEXd5", // RECURRING_PRICE_ID + quantity: 1, + tax_rates: ["txr_1HCjzTHYgolSBA35m0e1tJN5"], + }, + { + price: "price_1HEtgDHYgolSBA35LMkO3ExX", // ONE_TIME_PRICE_ID + quantity: 1, + tax_rates: ["txr_1HCjzTHYgolSBA35m0e1tJN5"], + }, + ], + success_url: window.location.origin, + cancel_url: window.location.origin, + }); ``` **_NOTE_**: If you specify more than one recurring price in the `line_items` array, the subscription object in Cloud Firestore will list all recurring prices in the `prices` array. The `price` attribute on the subscription in Cloud Firestore will be equal to the first item in the `prices` array: `price === prices[0]`. @@ -457,14 +461,14 @@ In order for this to work, Firebase Authentication users need to be synced with Subscription details are synced to the `subscriptions` sub-collection in the user's corresponding customer doc. ```js -db.collection('${param:CUSTOMERS_COLLECTION}') +db.collection("${param:CUSTOMERS_COLLECTION}") .doc(currentUser.uid) - .collection('subscriptions') - .where('status', 'in', ['trialing', 'active']) + .collection("subscriptions") + .where("status", "in", ["trialing", "active"]) .onSnapshot(async (snapshot) => { // In this implementation we only expect one active or trialing subscription to exist. const doc = snapshot.docs[0]; - console.log(doc.id, ' => ', doc.data()); + console.log(doc.id, " => ", doc.data()); }); ``` @@ -475,8 +479,8 @@ Once a customer is subscribed you should show them a button to access the custom ```js const functionRef = firebase .app() - .functions('${param:LOCATION}') - .httpsCallable('${function:createPortalLink.name}'); + .functions("${param:LOCATION}") + .httpsCallable("${function:createPortalLink.name}"); const { data } = await functionRef({ returnUrl: window.location.origin, locale: "auto", // Optional, defaults to "auto" diff --git a/firestore-stripe-payments/PREINSTALL.md b/firestore-stripe-payments/PREINSTALL.md index d6a11c5f..9e842563 100644 --- a/firestore-stripe-payments/PREINSTALL.md +++ b/firestore-stripe-payments/PREINSTALL.md @@ -19,9 +19,9 @@ The design for Stripe Checkout and the customer portal can be customized in your #### Recommended usage -If you're building on the web platform, you can use this extension for any of your payment use cases. +If you're building on the web platform, you can use this extension for any of your payment use cases. -If you're developing native mobile applications and you're selling digital products or services within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use the app store's in-app purchase APIs. See [Apple's](https://developer.apple.com/app-store/review/guidelines/#payments) and [Google's](https://support.google.com/googleplay/android-developer/answer/9858738?hl=en&ref_topic=9857752) guidelines for more information. +If you're developing native mobile applications and you're selling digital products or services within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use the app store's in-app purchase APIs. See [Apple's](https://developer.apple.com/app-store/review/guidelines/#payments) and [Google's](https://support.google.com/googleplay/android-developer/answer/9858738?hl=en&ref_topic=9857752) guidelines for more information. For all other scenarios you can use the [stripe-android](https://github.com/stripe/stripe-android), [stripe-ios](https://github.com/stripe/stripe-ios), [stripe-react-native](https://github.com/stripe/stripe-react-native), or [flutter_stripe](https://github.com/flutter-stripe/flutter_stripe) SDKs. @@ -73,4 +73,4 @@ You are responsible for any costs associated with your use of these services. To install this extension, your Firebase project must be on the Blaze (pay-as-you-go) plan. You will only be charged for the resources you use. Most Firebase services offer a free tier for low-volume use. [Learn more about Firebase billing.](https://firebase.google.com/pricing) -Starting August 17 2020, you will be billed a small amount (typically less than $0.10) when you install or reconfigure this extension. See the [Cloud Functions for Firebase billing FAQ](https://firebase.google.com/support/faq#expandable-15) for a detailed explanation. \ No newline at end of file +Starting August 17 2020, you will be billed a small amount (typically less than $0.10) when you install or reconfigure this extension. See the [Cloud Functions for Firebase billing FAQ](https://firebase.google.com/support/faq#expandable-15) for a detailed explanation. diff --git a/firestore-stripe-payments/README.md b/firestore-stripe-payments/README.md index e9bf11af..347da3cb 100644 --- a/firestore-stripe-payments/README.md +++ b/firestore-stripe-payments/README.md @@ -4,8 +4,6 @@ **Description**: Controls access to paid content by syncing your one-time and recurring payments with Firebase Authentication. - - **Details**: Use this extension as a backend for your [Stripe](https://www.stripe.com/) payments. The extension supports multiple use cases: @@ -27,9 +25,9 @@ The design for Stripe Checkout and the customer portal can be customized in your #### Recommended usage -If you're building on the web platform, you can use this extension for any of your payment use cases. +If you're building on the web platform, you can use this extension for any of your payment use cases. -If you're developing native mobile applications and you're selling digital products or services within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use the app store's in-app purchase APIs. See [Apple's](https://developer.apple.com/app-store/review/guidelines/#payments) and [Google's](https://support.google.com/googleplay/android-developer/answer/9858738?hl=en&ref_topic=9857752) guidelines for more information. +If you're developing native mobile applications and you're selling digital products or services within your app, (e.g. subscriptions, in-game currencies, game levels, access to premium content, or unlocking a full version), you must use the app store's in-app purchase APIs. See [Apple's](https://developer.apple.com/app-store/review/guidelines/#payments) and [Google's](https://support.google.com/googleplay/android-developer/answer/9858738?hl=en&ref_topic=9857752) guidelines for more information. For all other scenarios you can use the [stripe-android](https://github.com/stripe/stripe-android), [stripe-ios](https://github.com/stripe/stripe-ios), [stripe-react-native](https://github.com/stripe/stripe-react-native), or [flutter_stripe](https://github.com/flutter-stripe/flutter_stripe) SDKs. @@ -60,49 +58,6 @@ 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: @@ -126,52 +81,44 @@ To install this extension, your Firebase project must be on the Blaze (pay-as-yo Starting August 17 2020, you will be billed a small amount (typically less than $0.10) when you install or reconfigure this extension. See the [Cloud Functions for Firebase billing FAQ](https://firebase.google.com/support/faq#expandable-15) for a detailed explanation. - - **Configuration Parameters:** -* Cloud Functions deployment location: Where do you want to deploy the functions created for this extension? You usually want a location close to your database. For help selecting a location, refer to the [location selection guide](https://firebase.google.com/docs/functions/locations). +- Cloud Functions deployment location: Where do you want to deploy the functions created for this extension? You usually want a location close to your database. For help selecting a location, refer to the [location selection guide](https://firebase.google.com/docs/functions/locations). -* Products and pricing plans collection: What is the path to the Cloud Firestore collection where the extension should store Stripe pricing plans? +- Products and pricing plans collection: What is the path to the Cloud Firestore collection where the extension should store Stripe pricing plans? -* Customer details and subscriptions collection: What is the path to the Cloud Firestore collection where the extension should store Stripe customer details? This can be the location of an existing user collection, the extension will not overwrite your existing data but rather merge the Stripe data into your existing `uid` docs. +- Customer details and subscriptions collection: What is the path to the Cloud Firestore collection where the extension should store Stripe customer details? This can be the location of an existing user collection, the extension will not overwrite your existing data but rather merge the Stripe data into your existing `uid` docs. -* Stripe configuration collection: What is the path to the Cloud Firestore collection where the extension should store Stripe configuration? +- Stripe configuration collection: What is the path to the Cloud Firestore collection where the extension should store Stripe configuration? -* Sync new users to Stripe customers and Cloud Firestore: Do you want to automatically sync new users to customer objects in Stripe? If set to 'Sync', the extension will create a new customer object in Stripe and add a new doc to the customer collection in Firestore when a new user signs up via Firebase Authentication. If set to 'Do not sync' (default), the extension will create the customer object "on the fly" with the first checkout session creation. +- Sync new users to Stripe customers and Cloud Firestore: Do you want to automatically sync new users to customer objects in Stripe? If set to 'Sync', the extension will create a new customer object in Stripe and add a new doc to the customer collection in Firestore when a new user signs up via Firebase Authentication. If set to 'Do not sync' (default), the extension will create the customer object "on the fly" with the first checkout session creation. -* Automatically delete Stripe customer objects: Do you want to automatically delete customer objects in Stripe? When a user is deleted in Firebase Authentication or in Cloud Firestore and set to 'Auto delete' the extension will delete their customer object in Stripe which will immediately cancel all subscriptions for the user. +- Automatically delete Stripe customer objects: Do you want to automatically delete customer objects in Stripe? When a user is deleted in Firebase Authentication or in Cloud Firestore and set to 'Auto delete' the extension will delete their customer object in Stripe which will immediately cancel all subscriptions for the user. -* Stripe API key with restricted access: What is your Stripe API key? We recommend creating a new [restricted key](https://stripe.com/docs/keys#limit-access) with write access only for the "Customers", "Checkout Sessions" and "Customer portal" resources. And read-only access for the "Subscriptions" and "Prices" resources. - -* Stripe webhook secret: This is your signing secret for a Stripe-registered webhook. This webhook can only be registered after installation. Leave this value untouched during installation, then follow the postinstall instructions for registering your webhook and configuring this value. - -* Minimum instances for createCheckoutSession function: Set the minimum number of function instances that should be always be available to create Checkout Sessions. This number can be adjusted to reduce cold starts and increase the responsiveness of Checkout Session creation requests. Suggested values are 0 or 1. Please note this setting will likely incur billing costss, see the [Firebase documentation](https://firebase.google.com/docs/functions/manage-functions#reduce_the_number_of_cold_starts) for more information. +- Stripe API key with restricted access: What is your Stripe API key? We recommend creating a new [restricted key](https://stripe.com/docs/keys#limit-access) with write access only for the "Customers", "Checkout Sessions" and "Customer portal" resources. And read-only access for the "Subscriptions" and "Prices" resources. +- Stripe webhook secret: This is your signing secret for a Stripe-registered webhook. This webhook can only be registered after installation. Leave this value untouched during installation, then follow the postinstall instructions for registering your webhook and configuring this value. +- Minimum instances for createCheckoutSession function: Set the minimum number of function instances that should be always be available to create Checkout Sessions. This number can be adjusted to reduce cold starts and increase the responsiveness of Checkout Session creation requests. Suggested values are 0 or 1. Please note this setting will likely incur billing costss, see the [Firebase documentation](https://firebase.google.com/docs/functions/manage-functions#reduce_the_number_of_cold_starts) for more information. **Cloud Functions:** -* **createCustomer:** Creates a Stripe customer object when a new user signs up. - -* **createCheckoutSession:** Creates a Checkout session to collect the customer's payment details. +- **createCustomer:** Creates a Stripe customer object when a new user signs up. -* **createPortalLink:** Creates links to the customer portal for the user to manage their payment & subscription details. +- **createCheckoutSession:** Creates a Checkout session to collect the customer's payment details. -* **handleWebhookEvents:** Handles Stripe webhook events to keep subscription statuses in sync and update custom claims. +- **createPortalLink:** Creates links to the customer portal for the user to manage their payment & subscription details. -* **onUserDeleted:** Deletes the Stripe customer object and cancels all their subscriptions when the user is deleted in Firebase Authentication. - -* **onCustomerDataDeleted:** Deletes the Stripe customer object and cancels all their subscriptions when the customer doc in Cloud Firestore is deleted. +- **handleWebhookEvents:** Handles Stripe webhook events to keep subscription statuses in sync and update custom claims. +- **onUserDeleted:** Deletes the Stripe customer object and cancels all their subscriptions when the user is deleted in Firebase Authentication. +- **onCustomerDataDeleted:** Deletes the Stripe customer object and cancels all their subscriptions when the customer doc in Cloud Firestore is deleted. **Access Required**: - - This extension will operate with the following project IAM roles: -* firebaseauth.admin (Reason: Allows the extension to set custom claims for users.) +- firebaseauth.admin (Reason: Allows the extension to set custom claims for users.) -* datastore.user (Reason: Allows the extension to store customers & subscriptions in Cloud Firestore.) +- datastore.user (Reason: Allows the extension to store customers & subscriptions in Cloud Firestore.) diff --git a/firestore-stripe-payments/extension.yaml b/firestore-stripe-payments/extension.yaml index 8604bfea..ec99e6ba 100644 --- a/firestore-stripe-payments/extension.yaml +++ b/firestore-stripe-payments/extension.yaml @@ -17,7 +17,9 @@ version: 0.3.8 specVersion: v1beta displayName: Run Payments with Stripe -description: Controls access to paid content by syncing your one-time and recurring payments with Firebase Authentication. +description: + Controls access to paid content by syncing your one-time and recurring + payments with Firebase Authentication. icon: icon.png @@ -48,7 +50,8 @@ roles: Allows the extension to set custom claims for users. - role: datastore.user reason: >- - Allows the extension to store customers & subscriptions in Cloud Firestore. + Allows the extension to store customers & subscriptions in Cloud + Firestore. resources: - name: createCustomer @@ -76,7 +79,8 @@ resources: - name: createPortalLink type: firebaseextensions.v1beta.function description: >- - Creates links to the customer portal for the user to manage their payment & subscription details. + Creates links to the customer portal for the user to manage their payment + & subscription details. properties: location: ${LOCATION} runtime: nodejs18 @@ -85,7 +89,8 @@ resources: - name: handleWebhookEvents type: firebaseextensions.v1beta.function description: >- - Handles Stripe webhook events to keep subscription statuses in sync and update custom claims. + Handles Stripe webhook events to keep subscription statuses in sync and + update custom claims. properties: location: ${LOCATION} runtime: nodejs18 @@ -94,7 +99,8 @@ resources: - name: onUserDeleted type: firebaseextensions.v1beta.function description: >- - Deletes the Stripe customer object and cancels all their subscriptions when the user is deleted in Firebase Authentication. + Deletes the Stripe customer object and cancels all their subscriptions + when the user is deleted in Firebase Authentication. properties: location: ${LOCATION} runtime: nodejs18 @@ -105,7 +111,8 @@ resources: - name: onCustomerDataDeleted type: firebaseextensions.v1beta.function description: >- - Deletes the Stripe customer object and cancels all their subscriptions when the customer doc in Cloud Firestore is deleted. + Deletes the Stripe customer object and cancels all their subscriptions + when the customer doc in Cloud Firestore is deleted. properties: location: ${LOCATION} runtime: nodejs18 @@ -117,10 +124,10 @@ params: - param: LOCATION label: Cloud Functions deployment location description: >- - Where do you want to deploy the functions created for this extension? - You usually want a location close to your database. - For help selecting a location, refer to the - [location selection guide](https://firebase.google.com/docs/functions/locations). + Where do you want to deploy the functions created for this extension? You + usually want a location close to your database. For help selecting a + location, refer to the [location selection + guide](https://firebase.google.com/docs/functions/locations). type: select options: - label: Iowa (us-central1) @@ -130,7 +137,7 @@ params: - label: Northern Virginia (us-east4) value: us-east4 - label: Oregon (us-west1) - value: us-west1 + value: us-west1 - label: Los Angeles (us-west2) value: us-west2 - label: Salt Lake City (us-west3) @@ -172,70 +179,83 @@ params: - param: PRODUCTS_COLLECTION label: Products and pricing plans collection description: >- - What is the path to the Cloud Firestore collection where the extension should store Stripe pricing plans? + What is the path to the Cloud Firestore collection where the extension + should store Stripe pricing plans? default: products validationRegex: "^[^/]+(/[^/]+/[^/]+)*$" - validationErrorMessage: Firestore collection paths must be an odd number of segments separated by slashes, e.g. "path/to/collection". + validationErrorMessage: + Firestore collection paths must be an odd number of segments separated by + slashes, e.g. "path/to/collection". required: true - param: CUSTOMERS_COLLECTION label: Customer details and subscriptions collection description: >- - What is the path to the Cloud Firestore collection where the extension should store Stripe customer details? - This can be the location of an existing user collection, the extension will not overwrite your existing data but rather merge the Stripe data into your existing `uid` docs. + What is the path to the Cloud Firestore collection where the extension + should store Stripe customer details? This can be the location of an + existing user collection, the extension will not overwrite your existing + data but rather merge the Stripe data into your existing `uid` docs. default: customers validationRegex: "^[^/]+(/[^/]+/[^/]+)*$" - validationErrorMessage: Firestore collection paths must be an odd number of segments separated by slashes, e.g. "path/to/collection". + validationErrorMessage: + Firestore collection paths must be an odd number of segments separated by + slashes, e.g. "path/to/collection". required: true - param: STRIPE_CONFIG_COLLECTION label: Stripe configuration collection description: >- - What is the path to the Cloud Firestore collection where the extension should store Stripe configuration? + What is the path to the Cloud Firestore collection where the extension + should store Stripe configuration? default: configuration validationRegex: "^[^/]+(/[^/]+/[^/]+)*$" - validationErrorMessage: Firestore collection paths must be an odd number of segments separated by slashes, e.g. "path/to/collection". + validationErrorMessage: + Firestore collection paths must be an odd number of segments separated by + slashes, e.g. "path/to/collection". required: false - param: SYNC_USERS_ON_CREATE label: Sync new users to Stripe customers and Cloud Firestore description: >- Do you want to automatically sync new users to customer objects in Stripe? - If set to 'Sync', the extension will create a new customer object in Stripe - and add a new doc to the customer collection in Firestore when a new user signs up via Firebase Authentication. - If set to 'Do not sync' (default), the extension will create the customer object "on the fly" with the first checkout session creation. + If set to 'Sync', the extension will create a new customer object in + Stripe and add a new doc to the customer collection in Firestore when a + new user signs up via Firebase Authentication. If set to 'Do not sync' + (default), the extension will create the customer object "on the fly" with + the first checkout session creation. type: select options: - - label: 'Do not sync' - value: 'Do not sync' - - label: 'Sync' - value: 'Sync' - default: 'Do not sync' + - label: "Do not sync" + value: "Do not sync" + - label: "Sync" + value: "Sync" + default: "Do not sync" required: true - param: DELETE_STRIPE_CUSTOMERS label: Automatically delete Stripe customer objects description: >- - Do you want to automatically delete customer objects in Stripe? - When a user is deleted in Firebase Authentication or in Cloud Firestore and set to 'Auto delete' - the extension will delete their customer object in Stripe which will immediately cancel all subscriptions for the user. + Do you want to automatically delete customer objects in Stripe? When a + user is deleted in Firebase Authentication or in Cloud Firestore and set + to 'Auto delete' the extension will delete their customer object in Stripe + which will immediately cancel all subscriptions for the user. type: select options: - - label: 'Do not delete' - value: 'Do not delete' - - label: 'Auto delete' - value: 'Auto delete' - default: 'Do not delete' + - label: "Do not delete" + value: "Do not delete" + - label: "Auto delete" + value: "Auto delete" + default: "Do not delete" required: true - param: STRIPE_API_KEY label: Stripe API key with restricted access type: secret description: >- - What is your Stripe API key? - We recommend creating a new [restricted key](https://stripe.com/docs/keys#limit-access) - with write access only for the "Customers", "Checkout Sessions" and "Customer portal" resources. - And read-only access for the "Subscriptions" and "Prices" resources. + What is your Stripe API key? We recommend creating a new [restricted + key](https://stripe.com/docs/keys#limit-access) with write access only for + the "Customers", "Checkout Sessions" and "Customer portal" resources. And + read-only access for the "Subscriptions" and "Prices" resources. example: rk_live_1234567890 required: true @@ -243,20 +263,23 @@ params: label: Stripe webhook secret type: secret description: >- - This is your signing secret for a Stripe-registered webhook. - This webhook can only be registered after installation. - Leave this value untouched during installation, then follow the - postinstall instructions for registering your webhook - and configuring this value. + This is your signing secret for a Stripe-registered webhook. This webhook + can only be registered after installation. Leave this value untouched + during installation, then follow the postinstall instructions for + registering your webhook and configuring this value. example: whsec_1234567890 required: false - param: CREATE_CHECKOUT_SESSION_MIN_INSTANCES label: Minimum instances for createCheckoutSession function description: >- - Set the minimum number of function instances that should be always be available to create Checkout Sessions. - This number can be adjusted to reduce cold starts and increase the responsiveness - of Checkout Session creation requests. Suggested values are 0 or 1. Please note this setting will likely incur billing costss, see the [Firebase documentation](https://firebase.google.com/docs/functions/manage-functions#reduce_the_number_of_cold_starts) for more information. + Set the minimum number of function instances that should be always be + available to create Checkout Sessions. This number can be adjusted to + reduce cold starts and increase the responsiveness of Checkout Session + creation requests. Suggested values are 0 or 1. Please note this setting + will likely incur billing costss, see the [Firebase + documentation](https://firebase.google.com/docs/functions/manage-functions#reduce_the_number_of_cold_starts) + for more information. default: 0 required: true @@ -276,13 +299,18 @@ events: - type: com.stripe.v1.checkout.session.completed description: Occurs when a Checkout Session has been successfully completed. - type: com.stripe.v1.checkout.session.async_payment_succeeded - description: Occurs when a payment intent using a delayed payment method finally succeeds. + description: + Occurs when a payment intent using a delayed payment method finally + succeeds. - type: com.stripe.v1.checkout.session.async_payment_failed - description: Occurs when a payment intent using a delayed payment method fails. + description: + Occurs when a payment intent using a delayed payment method fails. - type: com.stripe.v1.customer.subscription.created description: Occurs whenever a customer is signed up for a new plan. - type: com.stripe.v1.customer.subscription.updated - description: Occurs whenever a subscription changes (e.g., switching from one plan to another, or changing the status from trial to active). + description: + Occurs whenever a subscription changes (e.g., switching from one plan to + another, or changing the status from trial to active). - type: com.stripe.v1.customer.subscription.deleted description: Occurs whenever a customer's subscription ends. - type: com.stripe.v1.tax_rate.created @@ -290,17 +318,26 @@ events: - type: com.stripe.v1.tax_rate.updated description: Occurs whenever a tax rate is updated. - type: com.stripe.v1.invoice.paid - description: Occurs whenever an invoice payment attempt succeeds or an invoice is marked as paid out-of-band. + description: + Occurs whenever an invoice payment attempt succeeds or an invoice is + marked as paid out-of-band. - type: com.stripe.v1.invoice.payment_succeeded description: Occurs whenever an invoice payment attempt succeeds. - type: com.stripe.v1.invoice.payment_failed - description: Occurs whenever an invoice payment attempt fails, due either to a declined payment or to the lack of a stored payment method. + description: + Occurs whenever an invoice payment attempt fails, due either to a declined + payment or to the lack of a stored payment method. - type: com.stripe.v1.invoice.upcoming - description: Occurs X number of days before a subscription is scheduled to create an invoice that is automatically charged—where X is determined by your subscriptions settings. + description: + Occurs X number of days before a subscription is scheduled to create an + invoice that is automatically charged—where X is determined by your + subscriptions settings. - type: com.stripe.v1.invoice.marked_uncollectible description: Occurs whenever an invoice is marked uncollectible. - type: com.stripe.v1.invoice.payment_action_required - description: Occurs whenever an invoice payment attempt requires further user action to complete. + description: + Occurs whenever an invoice payment attempt requires further user action to + complete. - type: com.stripe.v1.payment_intent.processing description: Occurs when a PaymentIntent has started processing. - type: com.stripe.v1.payment_intent.succeeded @@ -308,4 +345,6 @@ events: - type: com.stripe.v1.payment_intent.canceled description: Occurs when a PaymentIntent is canceled. - type: com.stripe.v1.payment_intent.payment_failed - description: Occurs when a PaymentIntent has failed the attempt to create a payment method or a payment. + description: + Occurs when a PaymentIntent has failed the attempt to create a payment + method or a payment. diff --git a/firestore-stripe-payments/functions/package.json b/firestore-stripe-payments/functions/package.json index c9f194ff..e14d8b8f 100644 --- a/firestore-stripe-payments/functions/package.json +++ b/firestore-stripe-payments/functions/package.json @@ -38,7 +38,6 @@ "dotenv": "^16.0.0", "envfile": "^6.17.0", "firebase-functions-test": "^0.3.3", - "localtunnel": "^2.0.2", "mocked-env": "^1.3.5", "ngrok": "^4.3.1", diff --git a/firestore-stripe-payments/functions/src/index.ts b/firestore-stripe-payments/functions/src/index.ts index ed5e451b..74a72472 100644 --- a/firestore-stripe-payments/functions/src/index.ts +++ b/firestore-stripe-payments/functions/src/index.ts @@ -70,6 +70,7 @@ const createCustomerRecord = async ({ if (email) customerData.email = email; if (phone) customerData.phone = phone; const customer = await stripe.customers.create(customerData); + // Add a mapping record in Cloud Firestore. const customerRecord = { email: customer.email, @@ -133,7 +134,7 @@ exports.createCheckoutSession = functions tax_rates = [], tax_id_collection = false, allow_promotion_codes = false, - trial_from_plan = true, + trial_period_days, line_items, billing_address_collection = 'required', collect_shipping_address = false, @@ -209,9 +210,12 @@ exports.createCheckoutSession = functions sessionCreateParams.payment_method_collection = payment_method_collection; sessionCreateParams.subscription_data = { - trial_from_plan, metadata, }; + if (trial_period_days) { + sessionCreateParams.subscription_data.trial_period_days = + trial_period_days; + } if (!automatic_tax) { sessionCreateParams.subscription_data.default_tax_rates = tax_rates; } @@ -301,6 +305,7 @@ exports.createCheckoutSession = functions const subscription = await stripe.subscriptions.create({ customer, items: [{ price }], + trial_period_days: trial_period_days, payment_behavior: 'default_incomplete', expand: ['latest_invoice.payment_intent'], metadata: {