Skip to content

Commit 3d16a7c

Browse files
committed
feat: Added Payment and getCurrentUserPayment APIs
1 parent 206e5a8 commit 3d16a7c

File tree

6 files changed

+593
-1
lines changed

6 files changed

+593
-1
lines changed

firestore-stripe-web-sdk/etc/firestore-stripe-payments.api.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export interface CreateCheckoutSessionOptions {
4141
timeoutMillis?: number;
4242
}
4343

44+
// @public
45+
export function getCurrentUserPayment(payments: StripePayments, paymentId: string): Promise<Payment>;
46+
4447
// @public
4548
export function getCurrentUserSubscription(payments: StripePayments, subscriptionId: string): Promise<Subscription>;
4649

@@ -102,9 +105,37 @@ export interface LineItemSessionCreateParams extends CommonSessionCreateParams {
102105
// @public
103106
export function onCurrentUserSubscriptionUpdate(payments: StripePayments, onUpdate: (snapshot: SubscriptionSnapshot) => void, onError?: (error: StripePaymentsError) => void): () => void;
104107

108+
// @public
109+
export interface Payment {
110+
// (undocumented)
111+
readonly [propName: string]: any;
112+
readonly amount: number;
113+
readonly amount_capturable: number;
114+
readonly amount_received: number;
115+
readonly created: string;
116+
readonly currency: string;
117+
readonly customer: string | null;
118+
readonly description: string | null;
119+
readonly id: string;
120+
readonly invoice: string | null;
121+
readonly metadata: {
122+
[name: string]: string;
123+
};
124+
readonly payment_method_types: string[];
125+
readonly prices: Array<{
126+
product: string;
127+
price: string;
128+
}>;
129+
readonly status: PaymentState;
130+
readonly uid: string;
131+
}
132+
105133
// @public
106134
export type PaymentMethodType = "card" | "acss_debit" | "afterpay_clearpay" | "alipay" | "bacs_debit" | "bancontact" | "boleto" | "eps" | "fpx" | "giropay" | "grabpay" | "ideal" | "klarna" | "oxxo" | "p24" | "sepa_debit" | "sofort" | "wechat_pay";
107135

136+
// @public
137+
export type PaymentState = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'cancelled' | 'succeeded';
138+
108139
// @public
109140
export interface Price {
110141
// (undocumented)

firestore-stripe-web-sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export {
3838
SessionCreateParams,
3939
} from "./session";
4040

41+
export { Payment, PaymentState, getCurrentUserPayment } from "./payment";
42+
4143
export {
4244
GetProductOptions,
4345
GetProductsOptions,
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright 2021 Google LLC
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 { FirebaseApp } from "@firebase/app";
18+
import {
19+
doc,
20+
DocumentData,
21+
DocumentReference,
22+
DocumentSnapshot,
23+
Firestore,
24+
FirestoreDataConverter,
25+
getDoc,
26+
getFirestore,
27+
QueryDocumentSnapshot,
28+
} from "@firebase/firestore";
29+
import { StripePayments, StripePaymentsError } from "./init";
30+
import { getCurrentUser } from "./user";
31+
import { checkNonEmptyString } from "./utils";
32+
33+
/**
34+
* Interface of a Stripe payment stored in the app database.
35+
*/
36+
export interface Payment {
37+
/**
38+
* Amount intended to be collected by this payment. A positive integer representing how much
39+
* to charge in the smallest currency unit (e.g., 100 cents to charge $1.00 or 100 to charge
40+
* ¥100, a zero-decimal currency). The minimum amount is $0.50 US or equivalent in charge
41+
* currency. The amount value supports up to eight digits (e.g., a value of 99999999 for a
42+
* USD charge of $999,999.99).
43+
*/
44+
readonly amount: number;
45+
46+
/**
47+
* Amount that can be captured from this payment.
48+
*/
49+
readonly amount_capturable: number;
50+
51+
/**
52+
* Amount that was collected by this payment.
53+
*/
54+
readonly amount_received: number;
55+
56+
/**
57+
* The date when the payment was created as a UTC timestamp.
58+
*/
59+
readonly created: string;
60+
61+
/**
62+
* Three-letter ISO currency code, in lowercase. Must be a supported currency.
63+
*/
64+
readonly currency: string;
65+
66+
/**
67+
* ID of the Customer this payment belongs to, if one exists. Payment methods attached
68+
* to other Customers cannot be used with this payment.
69+
*/
70+
readonly customer: string | null;
71+
72+
/**
73+
* An arbitrary string attached to the object. Often useful for displaying to users.
74+
*/
75+
readonly description: string | null;
76+
77+
/**
78+
* Unique Stripe payment ID.
79+
*/
80+
readonly id: string;
81+
82+
/**
83+
* ID of the invoice that created this payment, if it exists.
84+
*/
85+
readonly invoice: string | null;
86+
87+
/**
88+
* Set of key-value pairs that you can attach to an object. This can be useful for storing
89+
* additional information about the object in a structured format.
90+
*/
91+
readonly metadata: { [name: string]: string };
92+
93+
/**
94+
* The list of payment method types (e.g. card) that this payment is allowed to use.
95+
*/
96+
readonly payment_method_types: string[];
97+
98+
/**
99+
* Array of product ID and price ID pairs.
100+
*/
101+
readonly prices: Array<{ product: string; price: string }>;
102+
103+
/**
104+
* Status of this payment.
105+
*/
106+
readonly status: PaymentState;
107+
108+
/**
109+
* Firebase Auth UID of the user that created the payment.
110+
*/
111+
readonly uid: string;
112+
113+
readonly [propName: string]: any;
114+
}
115+
116+
/**
117+
* Possible states a payment can be in.
118+
*/
119+
export type PaymentState =
120+
| "requires_payment_method"
121+
| "requires_confirmation"
122+
| "requires_action"
123+
| "processing"
124+
| "requires_capture"
125+
| "cancelled"
126+
| "succeeded";
127+
128+
/**
129+
* Retrieves an existing Stripe payment for the currently signed in user from the database.
130+
*
131+
* @param payments - A valid {@link StripePayments} object.
132+
* @param subscriptionId - ID of the payment to retrieve.
133+
* @returns Resolves with a Payment object if found. Rejects if the specified payment ID
134+
* does not exist, or if the user is not signed in.
135+
*/
136+
export function getCurrentUserPayment(
137+
payments: StripePayments,
138+
paymentId: string
139+
): Promise<Payment> {
140+
checkNonEmptyString(paymentId, "paymentId must be a non-empty string.");
141+
return getCurrentUser(payments).then((uid: string) => {
142+
const dao: PaymentDAO = getOrInitPaymentDAO(payments);
143+
return dao.getPayment(uid, paymentId);
144+
});
145+
}
146+
147+
/**
148+
* Internal interface for all database interactions pertaining to Stripe payments. Exported
149+
* for testing.
150+
*
151+
* @internal
152+
*/
153+
export interface PaymentDAO {
154+
getPayment(uid: string, paymentId: string): Promise<Payment>;
155+
}
156+
157+
const PAYMENT_CONVERTER: FirestoreDataConverter<Payment> = {
158+
toFirestore: () => {
159+
throw new Error("Not implemented for readonly Payment type.");
160+
},
161+
fromFirestore: (snapshot: QueryDocumentSnapshot): Payment => {
162+
const data: DocumentData = snapshot.data();
163+
const refs: DocumentReference[] = data.prices;
164+
const prices: Array<{ product: string; price: string }> = refs.map(
165+
(priceRef: DocumentReference) => {
166+
return {
167+
product: priceRef.parent.parent!.id,
168+
price: priceRef.id,
169+
};
170+
}
171+
);
172+
173+
return {
174+
amount: data.amount,
175+
amount_capturable: data.amount_capturable,
176+
amount_received: data.amount_received,
177+
created: toUTCDateString(data.created),
178+
currency: data.currency,
179+
customer: data.customer,
180+
description: data.description,
181+
id: snapshot.id,
182+
invoice: data.invoice,
183+
metadata: data.metadata ?? {},
184+
payment_method_types: data.payment_method_types,
185+
prices,
186+
status: data.status,
187+
uid: snapshot.ref.parent.parent!.id,
188+
};
189+
},
190+
};
191+
192+
function toUTCDateString(seconds: number): string {
193+
const date = new Date(seconds * 1000);
194+
return date.toUTCString();
195+
}
196+
197+
const PAYMENTS_COLLECTION = "payments" as const;
198+
199+
class FirestorePaymentDAO implements PaymentDAO {
200+
private readonly firestore: Firestore;
201+
202+
constructor(app: FirebaseApp, private readonly customersCollection: string) {
203+
this.firestore = getFirestore(app);
204+
}
205+
206+
public async getPayment(uid: string, paymentId: string): Promise<Payment> {
207+
const snap: QueryDocumentSnapshot<Payment> =
208+
await this.getPaymentSnapshotIfExists(uid, paymentId);
209+
return snap.data();
210+
}
211+
212+
private async getPaymentSnapshotIfExists(
213+
uid: string,
214+
paymentId: string
215+
): Promise<QueryDocumentSnapshot<Payment>> {
216+
const paymentRef: DocumentReference<Payment> = doc(
217+
this.firestore,
218+
this.customersCollection,
219+
uid,
220+
PAYMENTS_COLLECTION,
221+
paymentId
222+
).withConverter(PAYMENT_CONVERTER);
223+
const snapshot: DocumentSnapshot<Payment> = await this.queryFirestore(() =>
224+
getDoc(paymentRef)
225+
);
226+
if (!snapshot.exists()) {
227+
throw new StripePaymentsError(
228+
"not-found",
229+
`No payment found with the ID: ${paymentId} for user: ${uid}`
230+
);
231+
}
232+
233+
return snapshot;
234+
}
235+
236+
private async queryFirestore<T>(fn: () => Promise<T>): Promise<T> {
237+
try {
238+
return await fn();
239+
} catch (error) {
240+
throw new StripePaymentsError(
241+
"internal",
242+
"Unexpected error while querying Firestore",
243+
error
244+
);
245+
}
246+
}
247+
}
248+
249+
const PAYMENT_DAO_KEY = "payment-dao" as const;
250+
251+
function getOrInitPaymentDAO(payments: StripePayments): PaymentDAO {
252+
let dao: PaymentDAO | null =
253+
payments.getComponent<PaymentDAO>(PAYMENT_DAO_KEY);
254+
if (!dao) {
255+
dao = new FirestorePaymentDAO(payments.app, payments.customersCollection);
256+
setPaymentDAO(payments, dao);
257+
}
258+
259+
return dao;
260+
}
261+
262+
/**
263+
* Internal API for registering a {@link PaymentDAO} instance with {@link StripePayments}.
264+
* Exported for testing.
265+
*
266+
* @internal
267+
*/
268+
export function setPaymentDAO(payments: StripePayments, dao: PaymentDAO): void {
269+
payments.setComponent(PAYMENT_DAO_KEY, dao);
270+
}

0 commit comments

Comments
 (0)