UNPKG

@invertase/firestore-stripe-payments

Version:

Client SDK for the firestore-stripe-payments Firebase Extension

846 lines (842 loc) 24.2 kB
import { registerVersion } from 'firebase/app'; import { getFirestore, collection, addDoc, onSnapshot, doc, getDoc, query, where, getDocs, limit } from 'firebase/firestore'; import { getAuth } from 'firebase/auth'; // src/init.ts registerVersion("firestore-stripe-payments", "__VERSION__"); function getStripePayments(app, options) { return StripePayments.create(app, options); } var StripePayments = class _StripePayments { constructor(app, options) { this.app = app; this.options = options; this.components = {}; } /** * @internal */ static create(app, options) { return new _StripePayments(app, options); } /** * Name of the customers collection as configured in the extension. */ get customersCollection() { return this.options.customersCollection; } /** * Name of the products collection as configured in the extension. */ get productsCollection() { return this.options.productsCollection; } /** * @internal */ getComponent(key) { let dao = this.components[key]; if (dao) { return dao; } return null; } /** * @internal */ setComponent(key, dao) { this.components[key] = dao; } }; var StripePaymentsError = class extends Error { constructor(code, message, cause) { super(message); this.code = code; this.message = message; this.cause = cause; } }; function getCurrentUser(payments) { try { const uid = getCurrentUserSync(payments); return Promise.resolve(uid); } catch (err) { return Promise.reject(err); } } function getCurrentUserSync(payments) { const dao = getOrInitUserDAO(payments); return dao.getCurrentUser(); } var FirebaseAuthUserDAO = class { constructor(app) { this.auth = getAuth(app); } getCurrentUser() { const currentUser = this.auth.currentUser?.uid; if (!currentUser) { throw new StripePaymentsError( "unauthenticated", "Failed to determine currently signed in user. User not signed in." ); } return currentUser; } }; var USER_DAO_KEY = "user-dao"; function getOrInitUserDAO(payments) { let dao = payments.getComponent(USER_DAO_KEY); if (!dao) { dao = new FirebaseAuthUserDAO(payments.app); setUserDAO(payments, dao); } return dao; } function setUserDAO(payments, dao) { payments.setComponent(USER_DAO_KEY, dao); } // src/utils.ts function checkNonEmptyString(arg, message) { if (typeof arg !== "string" || arg === "") { throw new Error(message ?? "arg must be a non-empty string."); } } function checkPositiveNumber(arg, message) { if (typeof arg !== "number" || isNaN(arg) || arg <= 0) { throw new Error(message ?? "arg must be positive number."); } } function checkNonEmptyArray(arg, message) { if (!Array.isArray(arg) || arg.length === 0) { throw new Error(message ?? "arg must be a non-empty array."); } } // src/session.ts function hasLineItems(params) { return "line_items" in params; } var CREATE_SESSION_TIMEOUT_MILLIS = 30 * 1e3; function createCheckoutSession(payments, params, options) { params = { ...params }; checkAndUpdateCommonParams(params); if (hasLineItems(params)) { checkLineItemParams(params); } else { checkPriceIdParams(params); } const timeoutMillis = getTimeoutMillis(options?.timeoutMillis); return getCurrentUser(payments).then((uid) => { const dao = getOrInitSessionDAO(payments); return dao.createCheckoutSession(uid, params, timeoutMillis); }); } function checkAndUpdateCommonParams(params) { if (typeof params.cancel_url !== "undefined") { checkNonEmptyString( params.cancel_url, "cancel_url must be a non-empty string." ); } else { params.cancel_url = window.location.href; } params.mode ?? (params.mode = "subscription"); if (typeof params.success_url !== "undefined") { checkNonEmptyString( params.success_url, "success_url must be a non-empty string." ); } else { params.success_url = window.location.href; } } function checkLineItemParams(params) { checkNonEmptyArray( params.line_items, "line_items must be a non-empty array." ); } function checkPriceIdParams(params) { checkNonEmptyString(params.price, "price must be a non-empty string."); if (typeof params.quantity !== "undefined") { checkPositiveNumber( params.quantity, "quantity must be a positive integer." ); } } function getTimeoutMillis(timeoutMillis) { if (typeof timeoutMillis !== "undefined") { checkPositiveNumber( timeoutMillis, "timeoutMillis must be a positive number." ); return timeoutMillis; } return CREATE_SESSION_TIMEOUT_MILLIS; } var FirestoreSessionDAO = class { constructor(app, customersCollection) { this.customersCollection = customersCollection; this.firestore = getFirestore(app); } async createCheckoutSession(uid, params, timeoutMillis) { const doc4 = await this.addSessionDoc(uid, params); return this.waitForSessionId(doc4, timeoutMillis); } async addSessionDoc(uid, params) { const sessions = collection( this.firestore, this.customersCollection, uid, "checkout_sessions" ); try { return await addDoc(sessions, params); } catch (err) { throw new StripePaymentsError( "internal", "Error while querying Firestore.", err ); } } waitForSessionId(doc4, timeoutMillis) { let cancel; return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject( new StripePaymentsError( "deadline-exceeded", "Timeout while waiting for session response." ) ); }, timeoutMillis); cancel = onSnapshot( doc4.withConverter(SESSION_CONVERTER), (snap) => { const session = snap.data(); if (hasSessionId(session)) { clearTimeout(timeout); resolve(session); } }, (err) => { clearTimeout(timeout); reject( new StripePaymentsError( "internal", "Error while querying Firestore.", err ) ); } ); }).finally(() => cancel()); } }; function hasSessionId(session) { return typeof session?.id !== "undefined"; } var SESSION_CONVERTER = { toFirestore: () => { throw new Error("Not implemented for readonly Session type."); }, fromFirestore: (snapshot) => { const { created, sessionId, ...rest } = snapshot.data(); if (typeof sessionId !== "undefined") { return { ...rest, id: sessionId, created_at: toUTCDateString(created) }; } return { ...rest }; } }; function toUTCDateString(timestamp) { return timestamp.toDate().toUTCString(); } var SESSION_DAO_KEY = "checkout-session-dao"; function getOrInitSessionDAO(payments) { let dao = payments.getComponent(SESSION_DAO_KEY); if (!dao) { dao = new FirestoreSessionDAO(payments.app, payments.customersCollection); setSessionDAO(payments, dao); } return dao; } function setSessionDAO(payments, dao) { payments.setComponent(SESSION_DAO_KEY, dao); } function getCurrentUserPayment(payments, paymentId) { checkNonEmptyString(paymentId, "paymentId must be a non-empty string."); return getCurrentUser(payments).then((uid) => { const dao = getOrInitPaymentDAO(payments); return dao.getPayment(uid, paymentId); }); } function getCurrentUserPayments(payments, options) { const queryOptions = {}; if (typeof options?.status !== "undefined") { queryOptions.status = getStatusAsArray(options.status); } return getCurrentUser(payments).then((uid) => { const dao = getOrInitPaymentDAO(payments); return dao.getPayments(uid, queryOptions); }); } function onCurrentUserPaymentUpdate(payments, onUpdate, onError) { const uid = getCurrentUserSync(payments); const dao = getOrInitPaymentDAO(payments); return dao.onPaymentUpdate(uid, onUpdate, onError); } function getStatusAsArray(status) { if (typeof status === "string") { return [status]; } checkNonEmptyArray(status, "status must be a non-empty array."); return status; } var PAYMENT_CONVERTER = { toFirestore: () => { throw new Error("Not implemented for readonly Payment type."); }, fromFirestore: (snapshot) => { const data = snapshot.data(); const refs = data.prices; const prices = refs.map( (priceRef) => { return { product: priceRef.parent.parent.id, price: priceRef.id }; } ); return { amount: data.amount, amount_capturable: data.amount_capturable, amount_received: data.amount_received, created: toUTCDateString2(data.created), currency: data.currency, customer: data.customer, description: data.description, id: snapshot.id, invoice: data.invoice, metadata: data.metadata ?? {}, payment_method_types: data.payment_method_types, prices, status: data.status, uid: snapshot.ref.parent.parent.id }; } }; function toUTCDateString2(seconds) { const date = new Date(seconds * 1e3); return date.toUTCString(); } var PAYMENTS_COLLECTION = "payments"; var FirestorePaymentDAO = class { constructor(app, customersCollection) { this.customersCollection = customersCollection; this.firestore = getFirestore(app); } async getPayment(uid, paymentId) { const snap = await this.getPaymentSnapshotIfExists(uid, paymentId); return snap.data(); } async getPayments(uid, options) { const querySnap = await this.getPaymentSnapshots( uid, options?.status ); const payments = []; querySnap.forEach((snap) => { payments.push(snap.data()); }); return payments; } onPaymentUpdate(uid, onUpdate, onError) { const payments = collection( this.firestore, this.customersCollection, uid, PAYMENTS_COLLECTION ).withConverter(PAYMENT_CONVERTER); return onSnapshot( payments, (querySnap) => { const snapshot = { payments: [], changes: [], size: querySnap.size, empty: querySnap.empty }; querySnap.forEach((snap) => { snapshot.payments.push(snap.data()); }); querySnap.docChanges().forEach((change) => { snapshot.changes.push({ type: change.type, payment: change.doc.data() }); }); onUpdate(snapshot); }, (err) => { if (onError) { const arg = new StripePaymentsError( "internal", `Error while listening to database updates: ${err.message}`, err ); onError(arg); } } ); } async getPaymentSnapshotIfExists(uid, paymentId) { const paymentRef = doc( this.firestore, this.customersCollection, uid, PAYMENTS_COLLECTION, paymentId ).withConverter(PAYMENT_CONVERTER); const snapshot = await this.queryFirestore( () => getDoc(paymentRef) ); if (!snapshot.exists()) { throw new StripePaymentsError( "not-found", `No payment found with the ID: ${paymentId} for user: ${uid}` ); } return snapshot; } async getPaymentSnapshots(uid, status) { let paymentsQuery = collection( this.firestore, this.customersCollection, uid, PAYMENTS_COLLECTION ).withConverter(PAYMENT_CONVERTER); if (status) { paymentsQuery = query(paymentsQuery, where("status", "in", status)); } return await this.queryFirestore(() => getDocs(paymentsQuery)); } async queryFirestore(fn) { try { return await fn(); } catch (error) { throw new StripePaymentsError( "internal", "Unexpected error while querying Firestore", error ); } } }; var PAYMENT_DAO_KEY = "payment-dao"; function getOrInitPaymentDAO(payments) { let dao = payments.getComponent(PAYMENT_DAO_KEY); if (!dao) { dao = new FirestorePaymentDAO(payments.app, payments.customersCollection); setPaymentDAO(payments, dao); } return dao; } function setPaymentDAO(payments, dao) { payments.setComponent(PAYMENT_DAO_KEY, dao); } function getProduct(payments, productId, options) { checkNonEmptyString(productId, "productId must be a non-empty string."); const dao = getOrInitProductDAO(payments); return dao.getProduct(productId).then((product) => { if (options?.includePrices) { return getProductWithPrices(dao, product); } return product; }); } function getProducts(payments, options) { const dao = getOrInitProductDAO(payments); const { includePrices, ...rest } = options ?? {}; return dao.getProducts(rest).then((products) => { if (includePrices) { const productsWithPrices = products.map( (product) => getProductWithPrices(dao, product) ); return Promise.all(productsWithPrices); } return products; }); } async function getProductWithPrices(dao, product) { const prices = await dao.getPrices(product.id); return { ...product, prices }; } function getPrice(payments, productId, priceId) { checkNonEmptyString(productId, "productId must be a non-empty string."); checkNonEmptyString(priceId, "priceId must be a non-empty string."); const dao = getOrInitProductDAO(payments); return dao.getPrice(productId, priceId); } function getPrices(payments, productId) { checkNonEmptyString(productId, "productId must be a non-empty string."); const dao = getOrInitProductDAO(payments); return dao.getPrices(productId, { assertProduct: true }); } var PRODUCT_CONVERTER = { toFirestore: () => { throw new Error("Not implemented for readonly Product type."); }, fromFirestore: (snapshot) => { return { ...snapshot.data(), id: snapshot.id, prices: [] }; } }; var PRICE_CONVERTER = { toFirestore: () => { throw new Error("Not implemented for readonly Price type."); }, fromFirestore: (snapshot) => { const data = snapshot.data(); return { ...data, id: snapshot.id, product: snapshot.ref.parent.parent.id }; } }; var FirestoreProductDAO = class { constructor(app, productsCollection) { this.productsCollection = productsCollection; this.firestore = getFirestore(app); } async getProduct(productId) { const snap = await this.getProductSnapshotIfExists(productId); return snap.data(); } async getProducts(options) { const querySnap = await this.getProductSnapshots( options ); const products = []; querySnap.forEach((snap) => { products.push(snap.data()); }); return products; } async getPrice(productId, priceId) { const snap = await this.getPriceSnapshotIfExists(productId, priceId); return snap.data(); } async getPrices(productId, options) { if (options?.assertProduct) { await this.getProductSnapshotIfExists(productId); } const querySnap = await this.getPriceSnapshots( productId ); const prices = []; querySnap.forEach((snap) => { prices.push(snap.data()); }); return prices; } async getProductSnapshotIfExists(productId) { const productRef = doc( this.firestore, this.productsCollection, productId ).withConverter(PRODUCT_CONVERTER); const snapshot = await this.queryFirestore( () => getDoc(productRef) ); if (!snapshot.exists()) { throw new StripePaymentsError( "not-found", `No product found with the ID: ${productId}` ); } return snapshot; } async getProductSnapshots(options) { let productsQuery = collection( this.firestore, this.productsCollection ).withConverter(PRODUCT_CONVERTER); const constraints = []; if (options?.activeOnly) { constraints.push(where("active", "==", true)); } if (options?.where) { for (const filter of options.where) { constraints.push(where(...filter)); } } if (typeof options?.limit !== "undefined") { constraints.push(limit(options.limit)); } return await this.queryFirestore(() => { if (constraints.length > 0) { productsQuery = query(productsQuery, ...constraints); } return getDocs(productsQuery); }); } async getPriceSnapshotIfExists(productId, priceId) { const priceRef = doc( this.firestore, this.productsCollection, productId, "prices", priceId ).withConverter(PRICE_CONVERTER); const snapshot = await this.queryFirestore( () => getDoc(priceRef) ); if (!snapshot.exists()) { throw new StripePaymentsError( "not-found", `No price found with the product ID: ${productId} and price ID: ${priceId}` ); } return snapshot; } async getPriceSnapshots(productId) { const pricesCollection = collection( this.firestore, this.productsCollection, productId, "prices" ).withConverter(PRICE_CONVERTER); return await this.queryFirestore(() => getDocs(pricesCollection)); } async queryFirestore(fn) { try { return await fn(); } catch (error) { throw new StripePaymentsError( "internal", "Unexpected error while querying Firestore", error ); } } }; var PRODUCT_DAO_KEY = "product-dao"; function getOrInitProductDAO(payments) { let dao = payments.getComponent(PRODUCT_DAO_KEY); if (!dao) { dao = new FirestoreProductDAO(payments.app, payments.productsCollection); setProductDAO(payments, dao); } return dao; } function setProductDAO(payments, dao) { payments.setComponent(PRODUCT_DAO_KEY, dao); } function getCurrentUserSubscription(payments, subscriptionId) { checkNonEmptyString( subscriptionId, "subscriptionId must be a non-empty string." ); return getCurrentUser(payments).then((uid) => { const dao = getOrInitSubscriptionDAO(payments); return dao.getSubscription(uid, subscriptionId); }); } function getCurrentUserSubscriptions(payments, options) { const queryOptions = {}; if (typeof options?.status !== "undefined") { queryOptions.status = getStatusAsArray2(options.status); } return getCurrentUser(payments).then((uid) => { const dao = getOrInitSubscriptionDAO(payments); return dao.getSubscriptions(uid, queryOptions); }); } function onCurrentUserSubscriptionUpdate(payments, onUpdate, onError) { const uid = getCurrentUserSync(payments); const dao = getOrInitSubscriptionDAO(payments); return dao.onSubscriptionUpdate(uid, onUpdate, onError); } function getStatusAsArray2(status) { if (typeof status === "string") { return [status]; } checkNonEmptyArray(status, "status must be a non-empty array."); return status; } var SUBSCRIPTION_CONVERTER = { toFirestore: () => { throw new Error("Not implemented for readonly Subscription type."); }, fromFirestore: (snapshot) => { const data = snapshot.data(); const refs = data.prices; const prices = refs.map( (priceRef) => { return { product: priceRef.parent.parent.id, price: priceRef.id }; } ); return { cancel_at: toNullableUTCDateString(data.cancel_at), cancel_at_period_end: data.cancel_at_period_end, canceled_at: toNullableUTCDateString(data.canceled_at), created: toUTCDateString3(data.created), current_period_start: toUTCDateString3(data.current_period_start), current_period_end: toUTCDateString3(data.current_period_end), ended_at: toNullableUTCDateString(data.ended_at), id: snapshot.id, metadata: data.metadata ?? {}, price: data.price.id, prices, product: data.product.id, quantity: data.quantity ?? null, role: data.role ?? null, status: data.status, stripe_link: data.stripeLink, trial_end: toNullableUTCDateString(data.trial_end), trial_start: toNullableUTCDateString(data.trial_start), uid: snapshot.ref.parent.parent.id }; } }; var SUBSCRIPTIONS_COLLECTION = "subscriptions"; function toNullableUTCDateString(timestamp) { if (timestamp === null) { return null; } return toUTCDateString3(timestamp); } function toUTCDateString3(timestamp) { return timestamp.toDate().toUTCString(); } var FirestoreSubscriptionDAO = class { constructor(app, customersCollection) { this.customersCollection = customersCollection; this.firestore = getFirestore(app); } async getSubscription(uid, subscriptionId) { const snap = await this.getSubscriptionSnapshotIfExists(uid, subscriptionId); return snap.data(); } async getSubscriptions(uid, options) { const querySnap = await this.getSubscriptionSnapshots(uid, options?.status); const subscriptions = []; querySnap.forEach((snap) => { subscriptions.push(snap.data()); }); return subscriptions; } onSubscriptionUpdate(uid, onUpdate, onError) { const subscriptions = collection( this.firestore, this.customersCollection, uid, SUBSCRIPTIONS_COLLECTION ).withConverter(SUBSCRIPTION_CONVERTER); return onSnapshot( subscriptions, (querySnap) => { const snapshot = { subscriptions: [], changes: [], size: querySnap.size, empty: querySnap.empty }; querySnap.forEach((snap) => { snapshot.subscriptions.push(snap.data()); }); querySnap.docChanges().forEach((change) => { snapshot.changes.push({ type: change.type, subscription: change.doc.data() }); }); onUpdate(snapshot); }, (err) => { if (onError) { const arg = new StripePaymentsError( "internal", `Error while listening to database updates: ${err.message}`, err ); onError(arg); } } ); } async getSubscriptionSnapshotIfExists(uid, subscriptionId) { const subscriptionRef = doc( this.firestore, this.customersCollection, uid, SUBSCRIPTIONS_COLLECTION, subscriptionId ).withConverter(SUBSCRIPTION_CONVERTER); const snapshot = await this.queryFirestore( () => getDoc(subscriptionRef) ); if (!snapshot.exists()) { throw new StripePaymentsError( "not-found", `No subscription found with the ID: ${subscriptionId} for user: ${uid}` ); } return snapshot; } async getSubscriptionSnapshots(uid, status) { let subscriptionsQuery = collection( this.firestore, this.customersCollection, uid, SUBSCRIPTIONS_COLLECTION ).withConverter(SUBSCRIPTION_CONVERTER); if (status) { subscriptionsQuery = query( subscriptionsQuery, where("status", "in", status) ); } return await this.queryFirestore(() => getDocs(subscriptionsQuery)); } async queryFirestore(fn) { try { return await fn(); } catch (error) { throw new StripePaymentsError( "internal", "Unexpected error while querying Firestore", error ); } } }; var SUBSCRIPTION_DAO_KEY = "subscription-dao"; function getOrInitSubscriptionDAO(payments) { let dao = payments.getComponent(SUBSCRIPTION_DAO_KEY); if (!dao) { dao = new FirestoreSubscriptionDAO( payments.app, payments.customersCollection ); setSubscriptionDAO(payments, dao); } return dao; } function setSubscriptionDAO(payments, dao) { payments.setComponent(SUBSCRIPTION_DAO_KEY, dao); } export { CREATE_SESSION_TIMEOUT_MILLIS, StripePayments, StripePaymentsError, createCheckoutSession, getCurrentUserPayment, getCurrentUserPayments, getCurrentUserSubscription, getCurrentUserSubscriptions, getPrice, getPrices, getProduct, getProducts, getStripePayments, onCurrentUserPaymentUpdate, onCurrentUserSubscriptionUpdate }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map