@invertase/firestore-stripe-payments
Version:
Client SDK for the firestore-stripe-payments Firebase Extension
846 lines (842 loc) • 24.2 kB
JavaScript
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