16
16
17
17
import { FirebaseApp } from "@firebase/app" ;
18
18
import {
19
+ collection ,
20
+ CollectionReference ,
19
21
doc ,
22
+ DocumentChange ,
20
23
DocumentData ,
21
24
DocumentReference ,
22
25
DocumentSnapshot ,
23
26
Firestore ,
24
27
FirestoreDataConverter ,
28
+ FirestoreError ,
25
29
getDoc ,
30
+ getDocs ,
26
31
getFirestore ,
32
+ onSnapshot ,
33
+ query ,
34
+ Query ,
27
35
QueryDocumentSnapshot ,
36
+ QuerySnapshot ,
37
+ where ,
28
38
} from "@firebase/firestore" ;
29
39
import { StripePayments , StripePaymentsError } from "./init" ;
30
- import { getCurrentUser } from "./user" ;
31
- import { checkNonEmptyString } from "./utils" ;
40
+ import { getCurrentUser , getCurrentUserSync } from "./user" ;
41
+ import { checkNonEmptyArray , checkNonEmptyString } from "./utils" ;
32
42
33
43
/**
34
44
* Interface of a Stripe payment stored in the app database.
@@ -103,7 +113,7 @@ export interface Payment {
103
113
/**
104
114
* Status of this payment.
105
115
*/
106
- readonly status : PaymentState ;
116
+ readonly status : PaymentStatus ;
107
117
108
118
/**
109
119
* Firebase Auth UID of the user that created the payment.
@@ -116,7 +126,7 @@ export interface Payment {
116
126
/**
117
127
* Possible states a payment can be in.
118
128
*/
119
- export type PaymentState =
129
+ export type PaymentStatus =
120
130
| "requires_payment_method"
121
131
| "requires_confirmation"
122
132
| "requires_action"
@@ -144,6 +154,105 @@ export function getCurrentUserPayment(
144
154
} ) ;
145
155
}
146
156
157
+ /**
158
+ * Optional parameters for the {@link getCurrentUserPayments} function.
159
+ */
160
+ export interface GetPaymentsOptions {
161
+ /**
162
+ * Specify one or more payment status values to retrieve. When set only the payments
163
+ * with the given status are returned.
164
+ */
165
+ status ?: PaymentStatus | PaymentStatus [ ] ;
166
+ }
167
+
168
+ export function getCurrentUserPayments (
169
+ payments : StripePayments ,
170
+ options ?: GetPaymentsOptions
171
+ ) : Promise < Payment [ ] > {
172
+ const queryOptions : { status ?: PaymentStatus [ ] } = { } ;
173
+ if ( typeof options ?. status !== "undefined" ) {
174
+ queryOptions . status = getStatusAsArray ( options . status ) ;
175
+ }
176
+
177
+ return getCurrentUser ( payments ) . then ( ( uid : string ) => {
178
+ const dao : PaymentDAO = getOrInitPaymentDAO ( payments ) ;
179
+ return dao . getPayments ( uid , queryOptions ) ;
180
+ } ) ;
181
+ }
182
+
183
+ /**
184
+ * Different types of changes that may occur on a payment object.
185
+ */
186
+ export type PaymentChangeType = "added" | "modified" | "removed" ;
187
+
188
+ /**
189
+ * Represents the current state of a set of payments owned by a user.
190
+ */
191
+ export interface PaymentSnapshot {
192
+ /**
193
+ * A list of all currently available payments ordered by the payment ID. Empty
194
+ * if no payments are available.
195
+ */
196
+ payments : Payment [ ] ;
197
+
198
+ /**
199
+ * The list of changes in the payments since the last snapshot.
200
+ */
201
+ changes : Array < {
202
+ type : PaymentChangeType ;
203
+ payment : Payment ;
204
+ } > ;
205
+
206
+ /**
207
+ * Number of currently available payments. This is same as the length of the
208
+ * `payments` array in the snapshot.
209
+ */
210
+ size : number ;
211
+
212
+ /**
213
+ * True if there are no payments available. False whenever at least one payment is
214
+ * present. When True, the `payments` array is empty, and the `size` is 0.
215
+ */
216
+ empty : boolean ;
217
+ }
218
+
219
+ /**
220
+ * Registers a listener to receive payment update events for the currently signed in
221
+ * user. If the user is not signed in throws an `unauthenticated` error, and no listener is
222
+ * registered.
223
+ *
224
+ * Upon successful registration, the `onUpdate` callback will fire once with
225
+ * the current state of all the payments. From then onwards, each update to a payment
226
+ * will fire the `onUpdate` callback with the latest state of the payments.
227
+ *
228
+ * @param payments - A valid {@link StripePayments} object.
229
+ * @param onUpdate - A callback that will fire whenever the current user's payments
230
+ * are updated.
231
+ * @param onError - A callback that will fire whenever an error occurs while listening to
232
+ * payment updates.
233
+ * @returns A function that can be called to cancel and unregister the listener.
234
+ */
235
+ export function onCurrentUserPaymentUpdate (
236
+ payments : StripePayments ,
237
+ onUpdate : ( snapshot : PaymentSnapshot ) => void ,
238
+ onError ?: ( error : StripePaymentsError ) => void
239
+ ) : ( ) => void {
240
+ const uid : string = getCurrentUserSync ( payments ) ;
241
+ const dao : PaymentDAO = getOrInitPaymentDAO ( payments ) ;
242
+ return dao . onPaymentUpdate ( uid , onUpdate , onError ) ;
243
+ }
244
+
245
+ function getStatusAsArray (
246
+ status : PaymentStatus | PaymentStatus [ ]
247
+ ) : PaymentStatus [ ] {
248
+ if ( typeof status === "string" ) {
249
+ return [ status ] ;
250
+ }
251
+
252
+ checkNonEmptyArray ( status , "status must be a non-empty array." ) ;
253
+ return status ;
254
+ }
255
+
147
256
/**
148
257
* Internal interface for all database interactions pertaining to Stripe payments. Exported
149
258
* for testing.
@@ -152,6 +261,15 @@ export function getCurrentUserPayment(
152
261
*/
153
262
export interface PaymentDAO {
154
263
getPayment ( uid : string , paymentId : string ) : Promise < Payment > ;
264
+ getPayments (
265
+ uid : string ,
266
+ options ?: { status ?: PaymentStatus [ ] }
267
+ ) : Promise < Payment [ ] > ;
268
+ onPaymentUpdate (
269
+ uid : string ,
270
+ onUpdate : ( snapshot : PaymentSnapshot ) => void ,
271
+ onError ?: ( error : StripePaymentsError ) => void
272
+ ) : ( ) => void ;
155
273
}
156
274
157
275
const PAYMENT_CONVERTER : FirestoreDataConverter < Payment > = {
@@ -209,6 +327,67 @@ class FirestorePaymentDAO implements PaymentDAO {
209
327
return snap . data ( ) ;
210
328
}
211
329
330
+ public async getPayments (
331
+ uid : string ,
332
+ options ?: { status ?: PaymentStatus [ ] }
333
+ ) : Promise < Payment [ ] > {
334
+ const querySnap : QuerySnapshot < Payment > = await this . getPaymentSnapshots (
335
+ uid ,
336
+ options ?. status
337
+ ) ;
338
+ const payments : Payment [ ] = [ ] ;
339
+ querySnap . forEach ( ( snap : QueryDocumentSnapshot < Payment > ) => {
340
+ payments . push ( snap . data ( ) ) ;
341
+ } ) ;
342
+
343
+ return payments ;
344
+ }
345
+
346
+ public onPaymentUpdate (
347
+ uid : string ,
348
+ onUpdate : ( snapshot : PaymentSnapshot ) => void ,
349
+ onError ?: ( error : StripePaymentsError ) => void
350
+ ) : ( ) => void {
351
+ const payments : CollectionReference < Payment > = collection (
352
+ this . firestore ,
353
+ this . customersCollection ,
354
+ uid ,
355
+ PAYMENTS_COLLECTION
356
+ ) . withConverter ( PAYMENT_CONVERTER ) ;
357
+ return onSnapshot (
358
+ payments ,
359
+ ( querySnap : QuerySnapshot < Payment > ) => {
360
+ const snapshot : PaymentSnapshot = {
361
+ payments : [ ] ,
362
+ changes : [ ] ,
363
+ size : querySnap . size ,
364
+ empty : querySnap . empty ,
365
+ } ;
366
+ querySnap . forEach ( ( snap : QueryDocumentSnapshot < Payment > ) => {
367
+ snapshot . payments . push ( snap . data ( ) ) ;
368
+ } ) ;
369
+ querySnap . docChanges ( ) . forEach ( ( change : DocumentChange < Payment > ) => {
370
+ snapshot . changes . push ( {
371
+ type : change . type ,
372
+ payment : change . doc . data ( ) ,
373
+ } ) ;
374
+ } ) ;
375
+
376
+ onUpdate ( snapshot ) ;
377
+ } ,
378
+ ( err : FirestoreError ) => {
379
+ if ( onError ) {
380
+ const arg : StripePaymentsError = new StripePaymentsError (
381
+ "internal" ,
382
+ `Error while listening to database updates: ${ err . message } ` ,
383
+ err
384
+ ) ;
385
+ onError ( arg ) ;
386
+ }
387
+ }
388
+ ) ;
389
+ }
390
+
212
391
private async getPaymentSnapshotIfExists (
213
392
uid : string ,
214
393
paymentId : string
@@ -233,6 +412,23 @@ class FirestorePaymentDAO implements PaymentDAO {
233
412
return snapshot ;
234
413
}
235
414
415
+ private async getPaymentSnapshots (
416
+ uid : string ,
417
+ status ?: PaymentStatus [ ]
418
+ ) : Promise < QuerySnapshot < Payment > > {
419
+ let paymentsQuery : Query < Payment > = collection (
420
+ this . firestore ,
421
+ this . customersCollection ,
422
+ uid ,
423
+ PAYMENTS_COLLECTION
424
+ ) . withConverter ( PAYMENT_CONVERTER ) ;
425
+ if ( status ) {
426
+ paymentsQuery = query ( paymentsQuery , where ( "status" , "in" , status ) ) ;
427
+ }
428
+
429
+ return await this . queryFirestore ( ( ) => getDocs ( paymentsQuery ) ) ;
430
+ }
431
+
236
432
private async queryFirestore < T > ( fn : ( ) => Promise < T > ) : Promise < T > {
237
433
try {
238
434
return await fn ( ) ;
0 commit comments