Skip to content

Commit 62eec4c

Browse files
committed
refactor: extract portal controller
1 parent f61ca5c commit 62eec4c

File tree

3 files changed

+362
-320
lines changed

3 files changed

+362
-320
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 { admin, stripe } from '../services';
20+
import * as logs from '../logs';
21+
import config from '../config';
22+
import { createCustomerRecord } from '../handlers/customer';
23+
24+
/**
25+
* Create a billing portal link
26+
*/
27+
export const createPortalLink = functions.https.onCall(
28+
async (data, context) => {
29+
// Checking that the user is authenticated.
30+
const uid = context.auth?.uid;
31+
if (!uid) {
32+
// Throwing an HttpsError so that the client gets the error details.
33+
throw new functions.https.HttpsError(
34+
'unauthenticated',
35+
'The function must be called while authenticated!'
36+
);
37+
}
38+
try {
39+
const {
40+
returnUrl: return_url,
41+
locale = 'auto',
42+
configuration,
43+
flow_data,
44+
} = data;
45+
46+
// Get stripe customer id
47+
let customerRecord = (
48+
await admin
49+
.firestore()
50+
.collection(config.customersCollectionPath)
51+
.doc(uid)
52+
.get()
53+
).data();
54+
55+
if (!customerRecord?.stripeId) {
56+
// Create Stripe customer on-the-fly
57+
const { email, phoneNumber } = await admin.auth().getUser(uid);
58+
customerRecord = await createCustomerRecord({
59+
uid,
60+
email,
61+
phone: phoneNumber,
62+
});
63+
}
64+
const customer = customerRecord.stripeId;
65+
66+
const params: Stripe.BillingPortal.SessionCreateParams = {
67+
customer,
68+
return_url,
69+
locale,
70+
};
71+
if (configuration) {
72+
params.configuration = configuration;
73+
}
74+
if (flow_data) {
75+
// Ignore type-checking because `flow_data` was added to
76+
// `Stripe.BillingPortal.SessionCreateParams` in
77+
// [email protected] (API version 2022-12-06)
78+
(params as any).flow_data = flow_data;
79+
}
80+
const session = await stripe.billingPortal.sessions.create(params);
81+
logs.createdBillingPortalLink(uid);
82+
return session;
83+
} catch (error) {
84+
logs.billingPortalLinkCreationError(uid, error);
85+
throw new functions.https.HttpsError('internal', error.message);
86+
}
87+
}
88+
);

0 commit comments

Comments
 (0)