|
| 1 | +/* |
| 2 | + * Copyright 2020 Stripe, Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +import * as functions from 'firebase-functions'; |
| 18 | +import Stripe from 'stripe'; |
| 19 | +import { Timestamp } from 'firebase-admin/firestore'; |
| 20 | +import { admin, stripe, apiVersion } from '../services'; |
| 21 | +import * as logs from '../logs'; |
| 22 | +import config from '../config'; |
| 23 | +import { createCustomerRecord } from '../handlers/customer'; |
| 24 | + |
| 25 | +/** |
| 26 | + * Create a CheckoutSession or PaymentIntent based on which client is being used. |
| 27 | + */ |
| 28 | +export const createCheckoutSession = functions |
| 29 | + .runWith({ |
| 30 | + minInstances: config.minCheckoutInstances, |
| 31 | + }) |
| 32 | + .firestore.document( |
| 33 | + `/${config.customersCollectionPath}/{uid}/checkout_sessions/{id}` |
| 34 | + ) |
| 35 | + .onCreate(async (snap, context) => { |
| 36 | + const { |
| 37 | + client = 'web', |
| 38 | + amount, |
| 39 | + currency, |
| 40 | + mode = 'subscription', |
| 41 | + price, |
| 42 | + success_url, |
| 43 | + cancel_url, |
| 44 | + quantity = 1, |
| 45 | + payment_method_types, |
| 46 | + shipping_rates = [], |
| 47 | + metadata = {}, |
| 48 | + automatic_payment_methods = { enabled: true }, |
| 49 | + automatic_tax = false, |
| 50 | + invoice_creation = false, |
| 51 | + tax_rates = [], |
| 52 | + tax_id_collection = false, |
| 53 | + allow_promotion_codes = false, |
| 54 | + trial_period_days, |
| 55 | + line_items, |
| 56 | + billing_address_collection = 'required', |
| 57 | + collect_shipping_address = false, |
| 58 | + customer_update = {}, |
| 59 | + locale = 'auto', |
| 60 | + promotion_code, |
| 61 | + client_reference_id, |
| 62 | + setup_future_usage, |
| 63 | + after_expiration = {}, |
| 64 | + consent_collection = {}, |
| 65 | + expires_at, |
| 66 | + phone_number_collection = {}, |
| 67 | + payment_method_collection = 'always', |
| 68 | + } = snap.data(); |
| 69 | + try { |
| 70 | + logs.creatingCheckoutSession(context.params.id); |
| 71 | + // Get stripe customer id |
| 72 | + let customerRecord = (await snap.ref.parent.parent.get()).data(); |
| 73 | + if (!customerRecord?.stripeId) { |
| 74 | + const { email, phoneNumber } = await admin |
| 75 | + .auth() |
| 76 | + .getUser(context.params.uid); |
| 77 | + customerRecord = await createCustomerRecord({ |
| 78 | + uid: context.params.uid, |
| 79 | + email, |
| 80 | + phone: phoneNumber, |
| 81 | + }); |
| 82 | + } |
| 83 | + const customer = customerRecord.stripeId; |
| 84 | + |
| 85 | + if (client === 'web') { |
| 86 | + // Get shipping countries |
| 87 | + const shippingCountries: Stripe.Checkout.SessionCreateParams.ShippingAddressCollection.AllowedCountry[] = |
| 88 | + collect_shipping_address |
| 89 | + ? ( |
| 90 | + await admin |
| 91 | + .firestore() |
| 92 | + .collection( |
| 93 | + config.stripeConfigCollectionPath || |
| 94 | + config.productsCollectionPath |
| 95 | + ) |
| 96 | + .doc('shipping_countries') |
| 97 | + .get() |
| 98 | + ).data()?.['allowed_countries'] ?? [] |
| 99 | + : []; |
| 100 | + const sessionCreateParams: Stripe.Checkout.SessionCreateParams = { |
| 101 | + billing_address_collection, |
| 102 | + shipping_address_collection: { allowed_countries: shippingCountries }, |
| 103 | + shipping_rates, |
| 104 | + customer, |
| 105 | + customer_update, |
| 106 | + line_items: line_items |
| 107 | + ? line_items |
| 108 | + : [ |
| 109 | + { |
| 110 | + price, |
| 111 | + quantity, |
| 112 | + }, |
| 113 | + ], |
| 114 | + mode, |
| 115 | + success_url, |
| 116 | + cancel_url, |
| 117 | + locale, |
| 118 | + after_expiration, |
| 119 | + consent_collection, |
| 120 | + phone_number_collection, |
| 121 | + ...(expires_at && { expires_at }), |
| 122 | + }; |
| 123 | + if (payment_method_types) { |
| 124 | + sessionCreateParams.payment_method_types = payment_method_types; |
| 125 | + } |
| 126 | + if (mode === 'subscription') { |
| 127 | + sessionCreateParams.payment_method_collection = |
| 128 | + payment_method_collection; |
| 129 | + sessionCreateParams.subscription_data = { |
| 130 | + metadata, |
| 131 | + }; |
| 132 | + if (trial_period_days) { |
| 133 | + sessionCreateParams.subscription_data.trial_period_days = |
| 134 | + trial_period_days; |
| 135 | + } |
| 136 | + if (!automatic_tax) { |
| 137 | + sessionCreateParams.subscription_data.default_tax_rates = tax_rates; |
| 138 | + } |
| 139 | + } else if (mode === 'payment') { |
| 140 | + sessionCreateParams.payment_intent_data = { |
| 141 | + metadata, |
| 142 | + ...(setup_future_usage && { setup_future_usage }), |
| 143 | + }; |
| 144 | + if (invoice_creation) { |
| 145 | + sessionCreateParams.invoice_creation = { |
| 146 | + enabled: true, |
| 147 | + }; |
| 148 | + } |
| 149 | + } |
| 150 | + if (automatic_tax) { |
| 151 | + sessionCreateParams.automatic_tax = { |
| 152 | + enabled: true, |
| 153 | + }; |
| 154 | + sessionCreateParams.customer_update.name = 'auto'; |
| 155 | + sessionCreateParams.customer_update.address = 'auto'; |
| 156 | + sessionCreateParams.customer_update.shipping = 'auto'; |
| 157 | + } |
| 158 | + if (tax_id_collection) { |
| 159 | + sessionCreateParams.tax_id_collection = { |
| 160 | + enabled: true, |
| 161 | + }; |
| 162 | + sessionCreateParams.customer_update.name = 'auto'; |
| 163 | + sessionCreateParams.customer_update.address = 'auto'; |
| 164 | + sessionCreateParams.customer_update.shipping = 'auto'; |
| 165 | + } |
| 166 | + if (promotion_code) { |
| 167 | + sessionCreateParams.discounts = [{ promotion_code }]; |
| 168 | + } else { |
| 169 | + sessionCreateParams.allow_promotion_codes = allow_promotion_codes; |
| 170 | + } |
| 171 | + if (client_reference_id) |
| 172 | + sessionCreateParams.client_reference_id = client_reference_id; |
| 173 | + const session = await stripe.checkout.sessions.create( |
| 174 | + sessionCreateParams, |
| 175 | + { idempotencyKey: context.params.id } |
| 176 | + ); |
| 177 | + await snap.ref.set( |
| 178 | + { |
| 179 | + client, |
| 180 | + mode, |
| 181 | + sessionId: session.id, |
| 182 | + url: session.url, |
| 183 | + created: Timestamp.now(), |
| 184 | + }, |
| 185 | + { merge: true } |
| 186 | + ); |
| 187 | + } else if (client === 'mobile') { |
| 188 | + let paymentIntentClientSecret = null; |
| 189 | + let setupIntentClientSecret = null; |
| 190 | + if (mode === 'payment') { |
| 191 | + if (!amount || !currency) { |
| 192 | + throw new Error( |
| 193 | + `When using 'client:mobile' and 'mode:payment' you must specify amount and currency!` |
| 194 | + ); |
| 195 | + } |
| 196 | + const paymentIntentCreateParams: Stripe.PaymentIntentCreateParams = { |
| 197 | + amount, |
| 198 | + currency, |
| 199 | + customer, |
| 200 | + metadata, |
| 201 | + ...(setup_future_usage && { setup_future_usage }), |
| 202 | + }; |
| 203 | + if (payment_method_types) { |
| 204 | + paymentIntentCreateParams.payment_method_types = |
| 205 | + payment_method_types; |
| 206 | + } else { |
| 207 | + paymentIntentCreateParams.automatic_payment_methods = |
| 208 | + automatic_payment_methods; |
| 209 | + } |
| 210 | + const paymentIntent = await stripe.paymentIntents.create( |
| 211 | + paymentIntentCreateParams |
| 212 | + ); |
| 213 | + paymentIntentClientSecret = paymentIntent.client_secret; |
| 214 | + } else if (mode === 'setup') { |
| 215 | + const setupIntent = await stripe.setupIntents.create({ |
| 216 | + customer, |
| 217 | + metadata, |
| 218 | + payment_method_types: payment_method_types ?? ['card'], |
| 219 | + }); |
| 220 | + setupIntentClientSecret = setupIntent.client_secret; |
| 221 | + } else if (mode === 'subscription') { |
| 222 | + const subscription = await stripe.subscriptions.create({ |
| 223 | + customer, |
| 224 | + items: [{ price }], |
| 225 | + trial_period_days: trial_period_days, |
| 226 | + payment_behavior: 'default_incomplete', |
| 227 | + expand: ['latest_invoice.payment_intent'], |
| 228 | + metadata: { |
| 229 | + firebaseUserUID: context.params.id, |
| 230 | + }, |
| 231 | + }); |
| 232 | + |
| 233 | + paymentIntentClientSecret = |
| 234 | + //@ts-ignore |
| 235 | + subscription.latest_invoice.payment_intent.client_secret; |
| 236 | + } else { |
| 237 | + throw new Error( |
| 238 | + `Mode '${mode} is not supported for 'client:mobile'!` |
| 239 | + ); |
| 240 | + } |
| 241 | + const ephemeralKey = await stripe.ephemeralKeys.create( |
| 242 | + { customer }, |
| 243 | + { apiVersion } |
| 244 | + ); |
| 245 | + await snap.ref.set( |
| 246 | + { |
| 247 | + client, |
| 248 | + mode, |
| 249 | + customer, |
| 250 | + created: Timestamp.now(), |
| 251 | + ephemeralKeySecret: ephemeralKey.secret, |
| 252 | + paymentIntentClientSecret, |
| 253 | + setupIntentClientSecret, |
| 254 | + }, |
| 255 | + { merge: true } |
| 256 | + ); |
| 257 | + } else { |
| 258 | + throw new Error( |
| 259 | + `Client ${client} is not supported. Only 'web' or ' mobile' is supported!` |
| 260 | + ); |
| 261 | + } |
| 262 | + logs.checkoutSessionCreated(context.params.id); |
| 263 | + return; |
| 264 | + } catch (error) { |
| 265 | + logs.checkoutSessionCreationError(context.params.id, error); |
| 266 | + await snap.ref.set( |
| 267 | + { error: { message: error.message } }, |
| 268 | + { merge: true } |
| 269 | + ); |
| 270 | + } |
| 271 | + }); |
0 commit comments