-
-
Notifications
You must be signed in to change notification settings - Fork 531
/
Copy pathstore.js
7091 lines (7081 loc) · 355 KB
/
store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var CdvPurchase;
(function (CdvPurchase) {
const ERROR_CODES_BASE = 6777000;
/**
* Error codes
*/
let ErrorCode;
(function (ErrorCode) {
/** Error: Failed to intialize the in-app purchase library */
ErrorCode[ErrorCode["SETUP"] = ERROR_CODES_BASE + 1] = "SETUP";
/** Error: Failed to load in-app products metadata */
ErrorCode[ErrorCode["LOAD"] = ERROR_CODES_BASE + 2] = "LOAD";
/** Error: Failed to make a purchase */
ErrorCode[ErrorCode["PURCHASE"] = ERROR_CODES_BASE + 3] = "PURCHASE";
/** Error: Failed to load the purchase receipt */
ErrorCode[ErrorCode["LOAD_RECEIPTS"] = ERROR_CODES_BASE + 4] = "LOAD_RECEIPTS";
/** Error: Client is not allowed to issue the request */
ErrorCode[ErrorCode["CLIENT_INVALID"] = ERROR_CODES_BASE + 5] = "CLIENT_INVALID";
/** Error: Purchase flow has been cancelled by user */
ErrorCode[ErrorCode["PAYMENT_CANCELLED"] = ERROR_CODES_BASE + 6] = "PAYMENT_CANCELLED";
/** Error: Something is suspicious about a purchase */
ErrorCode[ErrorCode["PAYMENT_INVALID"] = ERROR_CODES_BASE + 7] = "PAYMENT_INVALID";
/** Error: The user is not allowed to make a payment */
ErrorCode[ErrorCode["PAYMENT_NOT_ALLOWED"] = ERROR_CODES_BASE + 8] = "PAYMENT_NOT_ALLOWED";
/** Error: Unknown error */
ErrorCode[ErrorCode["UNKNOWN"] = ERROR_CODES_BASE + 10] = "UNKNOWN";
/** Error: Failed to refresh the purchase receipt */
ErrorCode[ErrorCode["REFRESH_RECEIPTS"] = ERROR_CODES_BASE + 11] = "REFRESH_RECEIPTS";
/** Error: The product identifier is invalid */
ErrorCode[ErrorCode["INVALID_PRODUCT_ID"] = ERROR_CODES_BASE + 12] = "INVALID_PRODUCT_ID";
/** Error: Cannot finalize a transaction or acknowledge a purchase */
ErrorCode[ErrorCode["FINISH"] = ERROR_CODES_BASE + 13] = "FINISH";
/** Error: Failed to communicate with the server */
ErrorCode[ErrorCode["COMMUNICATION"] = ERROR_CODES_BASE + 14] = "COMMUNICATION";
/** Error: Subscriptions are not available */
ErrorCode[ErrorCode["SUBSCRIPTIONS_NOT_AVAILABLE"] = ERROR_CODES_BASE + 15] = "SUBSCRIPTIONS_NOT_AVAILABLE";
/** Error: Purchase information is missing token */
ErrorCode[ErrorCode["MISSING_TOKEN"] = ERROR_CODES_BASE + 16] = "MISSING_TOKEN";
/** Error: Verification of store data failed */
ErrorCode[ErrorCode["VERIFICATION_FAILED"] = ERROR_CODES_BASE + 17] = "VERIFICATION_FAILED";
/** Error: Bad response from the server */
ErrorCode[ErrorCode["BAD_RESPONSE"] = ERROR_CODES_BASE + 18] = "BAD_RESPONSE";
/** Error: Failed to refresh the store */
ErrorCode[ErrorCode["REFRESH"] = ERROR_CODES_BASE + 19] = "REFRESH";
/** Error: Payment has expired */
ErrorCode[ErrorCode["PAYMENT_EXPIRED"] = ERROR_CODES_BASE + 20] = "PAYMENT_EXPIRED";
/** Error: Failed to download the content */
ErrorCode[ErrorCode["DOWNLOAD"] = ERROR_CODES_BASE + 21] = "DOWNLOAD";
/** Error: Failed to update a subscription */
ErrorCode[ErrorCode["SUBSCRIPTION_UPDATE_NOT_AVAILABLE"] = ERROR_CODES_BASE + 22] = "SUBSCRIPTION_UPDATE_NOT_AVAILABLE";
/** Error: The requested product is not available in the store. */
ErrorCode[ErrorCode["PRODUCT_NOT_AVAILABLE"] = ERROR_CODES_BASE + 23] = "PRODUCT_NOT_AVAILABLE";
/** Error: The user has not allowed access to Cloud service information */
ErrorCode[ErrorCode["CLOUD_SERVICE_PERMISSION_DENIED"] = ERROR_CODES_BASE + 24] = "CLOUD_SERVICE_PERMISSION_DENIED";
/** Error: The device could not connect to the network. */
ErrorCode[ErrorCode["CLOUD_SERVICE_NETWORK_CONNECTION_FAILED"] = ERROR_CODES_BASE + 25] = "CLOUD_SERVICE_NETWORK_CONNECTION_FAILED";
/** Error: The user has revoked permission to use this cloud service. */
ErrorCode[ErrorCode["CLOUD_SERVICE_REVOKED"] = ERROR_CODES_BASE + 26] = "CLOUD_SERVICE_REVOKED";
/** Error: The user has not yet acknowledged Apple’s privacy policy */
ErrorCode[ErrorCode["PRIVACY_ACKNOWLEDGEMENT_REQUIRED"] = ERROR_CODES_BASE + 27] = "PRIVACY_ACKNOWLEDGEMENT_REQUIRED";
/** Error: The app is attempting to use a property for which it does not have the required entitlement. */
ErrorCode[ErrorCode["UNAUTHORIZED_REQUEST_DATA"] = ERROR_CODES_BASE + 28] = "UNAUTHORIZED_REQUEST_DATA";
/** Error: The offer identifier is invalid. */
ErrorCode[ErrorCode["INVALID_OFFER_IDENTIFIER"] = ERROR_CODES_BASE + 29] = "INVALID_OFFER_IDENTIFIER";
/** Error: The price you specified in App Store Connect is no longer valid. */
ErrorCode[ErrorCode["INVALID_OFFER_PRICE"] = ERROR_CODES_BASE + 30] = "INVALID_OFFER_PRICE";
/** Error: The signature in a payment discount is not valid. */
ErrorCode[ErrorCode["INVALID_SIGNATURE"] = ERROR_CODES_BASE + 31] = "INVALID_SIGNATURE";
/** Error: Parameters are missing in a payment discount. */
ErrorCode[ErrorCode["MISSING_OFFER_PARAMS"] = ERROR_CODES_BASE + 32] = "MISSING_OFFER_PARAMS";
/**
* Server code used when a subscription expired.
*
* @deprecated Validator should now return the transaction in the collection as expired.
*/
ErrorCode[ErrorCode["VALIDATOR_SUBSCRIPTION_EXPIRED"] = 6778003] = "VALIDATOR_SUBSCRIPTION_EXPIRED";
})(ErrorCode = CdvPurchase.ErrorCode || (CdvPurchase.ErrorCode = {}));
/**
* Create an {@link IError} instance
*
* @internal
*/
function storeError(code, message, platform, productId) {
return { isError: true, code, message, platform, productId };
}
CdvPurchase.storeError = storeError;
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
/**
* Integrate with https://www.iaptic.com/
*
* @example
* const iaptic = new CdvPurchase.Iaptic({
* url: 'https://validator.iaptic.com',
* appName: 'test',
* apiKey: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
* });
* store.validator = iaptic.validator;
*/
class Iaptic {
constructor(config, store) {
this.config = config;
if (!config.url) {
config.url = 'https://validator.iaptic.com';
}
this.store = store !== null && store !== void 0 ? store : CdvPurchase.store;
this.log = this.store.log.child('Iaptic');
}
/**
* Provides a client token generated on iaptic's servers
*
* Can be passed to the Braintree Adapter at initialization.
*
* @example
* store.initialize([
* {
* platform: Platform.BRAINTREE,
* options: {
* clientTokenProvider: iaptic.braintreeClientTokenProvider
* }
* }
* ]);
*/
get braintreeClientTokenProvider() {
return callback => {
this.log.info('Calling Braintree clientTokenProvider');
CdvPurchase.Utils.ajax(this.log, {
url: `${this.config.url}/v3/braintree/client-token?appName=${this.config.appName}&apiKey=${this.config.apiKey}`,
method: 'POST',
data: {
applicationUsername: CdvPurchase.store.getApplicationUsername(),
customerId: CdvPurchase.Braintree.customerId,
},
success: body => {
this.log.info('clientTokenProvider success: ' + JSON.stringify(body));
callback(body.clientToken);
},
error: err => {
this.log.info('clientTokenProvider error: ' + JSON.stringify(err));
callback(CdvPurchase.storeError(err, 'ERROR ' + err, CdvPurchase.Platform.BRAINTREE, null));
},
});
};
}
/**
* Determine the eligibility of discounts based on the content of the application receipt.
*
* The secret sauce used here is to wait for validation of the application receipt.
* The receipt validator will return the necessary data to determine eligibility.
*
* Receipt validation is expected to happen after loading product information, so the implementation here is to
* wait for a validation response.
*/
get appStoreDiscountEligibilityDeterminer() {
// the user needs the appStoreDiscountEligibilityDeterminer, let's start listening to receipt validation events.
let latestReceipt;
this.log.debug("AppStore eligibility determiner is listening...");
this.store.when().verified(receipt => {
if (receipt.platform === CdvPurchase.Platform.APPLE_APPSTORE) {
this.log.debug("Got a verified AppStore receipt.");
latestReceipt = receipt;
}
}, 'appStoreDiscountEligibilityDeterminer_listening');
const determiner = (_appStoreReceipt, requests, callback) => {
this.log.debug("AppStore eligibility determiner");
if (latestReceipt) {
this.log.debug("Using cached receipt");
return callback(analyzeReceipt(latestReceipt, requests));
}
const onVerified = (receipt) => {
if (receipt.platform === CdvPurchase.Platform.APPLE_APPSTORE) {
this.log.debug("Receipt is verified, let's analyze the content and respond.");
this.store.off(onVerified);
callback(analyzeReceipt(receipt, requests));
}
};
this.log.debug("Waiting for receipt");
this.store.when().verified(onVerified, 'appStoreDiscountEligibilityDeterminer_waiting');
};
determiner.cacheReceipt = function (receipt) {
latestReceipt = receipt;
};
return determiner;
function analyzeReceipt(receipt, requests) {
const ineligibleIntro = receipt.raw.ineligible_for_intro_price;
return requests.map(request => {
var _a;
if (request.discountType === 'Introductory' && ineligibleIntro && ineligibleIntro.find(id => request.productId === id)) {
// User is not eligible for this introductory offer
return false;
}
else if (request.discountType === 'Subscription') {
// Discount only available if user is or was a subscriber
const matchingPurchase = (_a = receipt.raw.collection) === null || _a === void 0 ? void 0 : _a.find(purchase => purchase.id === request.productId);
return matchingPurchase ? true : false;
}
else {
// In other cases, assume the user is eligible
return true;
}
});
}
}
/** Validator URL */
get validator() {
return `${this.config.url}/v1/validate?appName=${this.config.appName}&apiKey=${this.config.apiKey}`;
}
}
CdvPurchase.Iaptic = Iaptic;
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
/**
* Desired logging level for the {@link Logger}
*
* @see {@link Store.verbosity}
*/
let LogLevel;
(function (LogLevel) {
/** Disable all logging (default) */
LogLevel[LogLevel["QUIET"] = 0] = "QUIET";
/** Show only error messages */
LogLevel[LogLevel["ERROR"] = 1] = "ERROR";
/** Show warnings and errors */
LogLevel[LogLevel["WARNING"] = 2] = "WARNING";
/** Also show information messages */
LogLevel[LogLevel["INFO"] = 3] = "INFO";
/** Enable internal debugging messages. */
LogLevel[LogLevel["DEBUG"] = 4] = "DEBUG";
})(LogLevel = CdvPurchase.LogLevel || (CdvPurchase.LogLevel = {}));
;
class Logger {
/** @internal */
constructor(store, prefix = '') {
/** All log lines are prefixed with this string */
this.prefix = '';
this.store = store;
this.prefix = prefix || 'CdvPurchase';
}
/**
* Create a child logger, whose prefix will be this one's + the given string.
*
* @example
* const log = store.log.child('AppStore')
*/
child(prefix) {
return new Logger(this.store, this.prefix + '.' + prefix);
}
/**
* Logs an error message, only if `store.verbosity` >= store.ERROR
*/
error(o) {
log(this.store.verbosity, LogLevel.ERROR, this.prefix, o);
// show the stack trace
try {
throw new Error(toString(o));
}
catch (e) {
log(this.store.verbosity, LogLevel.ERROR, this.prefix, e.stack);
}
}
/**
* Logs a warning message, only if `store.verbosity` >= store.WARNING
*/
warn(o) { log(this.store.verbosity, LogLevel.WARNING, this.prefix, o); }
/**
* Logs an info message, only if `store.verbosity` >= store.INFO
*/
info(o) { log(this.store.verbosity, LogLevel.INFO, this.prefix, o); }
/**
* Logs a debug message, only if `store.verbosity` >= store.DEBUG
*/
debug(o) { log(this.store.verbosity, LogLevel.DEBUG, this.prefix, o); }
/**
* Add warning logs on a console describing an exception.
*
* This method is mostly used when executing user registered callbacks.
*
* @param context - a string describing why the method was called
* @param error - a javascript Error object thrown by an exception
*/
logCallbackException(context, err) {
this.warn("A callback in \'" + context + "\' failed with an exception.");
if (typeof err === 'string')
this.warn(" " + err);
else if (err) {
const errAny = err;
if (errAny.fileName)
this.warn(" " + errAny.fileName + ":" + errAny.lineNumber);
if (err.message)
this.warn(" " + err.message);
if (err.stack)
this.warn(" " + err.stack);
}
}
}
/**
* Console object used to display log lines.
*
* It can be replaced by your implementation if you want to, for example, send logs to a remote server.
*
* @example
* Logger.console = {
* log: (message) => { remoteLog('LOG', message); },
* warn: (message) => { remoteLog('WARN', message); },
* error: (message) => { remoteLog('ERROR', message); }
* }
*/
Logger.console = window.console;
CdvPurchase.Logger = Logger;
const LOG_LEVEL_STRING = ["QUIET", "ERROR", "WARNING", "INFO", "DEBUG"];
function toString(o) {
if (typeof o !== 'string')
o = JSON.stringify(o);
return o;
}
function log(verbosity, level, prefix, o) {
var maxLevel = verbosity === true ? 1 : verbosity;
if (level > maxLevel)
return;
if (typeof o !== 'string')
o = JSON.stringify(o);
const fullPrefix = prefix ? `[${prefix}] ` : '';
const logStr = (level === LogLevel.ERROR) ? ((str) => Logger.console.error(str))
: (level === LogLevel.WARNING) ? ((str) => Logger.console.warn(str))
: ((str) => Logger.console.log(str));
if (LOG_LEVEL_STRING[level])
logStr(`${fullPrefix}${LOG_LEVEL_STRING[level]}: ${o}`);
else
logStr(`${fullPrefix}${o}`);
}
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
let Utils;
(function (Utils) {
Utils.nonEnumerable = (target, name, desc) => {
if (desc) {
desc.enumerable = false;
return desc;
}
Object.defineProperty(target, name, {
set(value) {
Object.defineProperty(this, name, {
value, writable: true, configurable: true,
});
},
configurable: true,
});
};
})(Utils = CdvPurchase.Utils || (CdvPurchase.Utils = {}));
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
/** Product definition from a store */
class Product {
/** @internal */
constructor(p, decorator) {
/** @internal */
this.className = 'Product';
/** Product title from the store. */
this.title = '';
/** Product full description from the store. */
this.description = '';
this.platform = p.platform;
this.type = p.type;
this.id = p.id;
this.group = p.group;
this.offers = [];
Object.defineProperty(this, 'pricing', { enumerable: false, get: () => { var _a; return (_a = this.offers[0]) === null || _a === void 0 ? void 0 : _a.pricingPhases[0]; } });
Object.defineProperty(this, 'canPurchase', { enumerable: false, get: () => decorator.canPurchase(this) });
Object.defineProperty(this, 'owned', { enumerable: false, get: () => decorator.owned(this) });
}
/**
* Shortcut to offers[0].pricingPhases[0]
*
* Useful when you know products have a single offer and a single pricing phase.
*/
get pricing() {
var _a;
// see Object.defineProperty in the constructor for the actual implementation.
return (_a = this.offers[0]) === null || _a === void 0 ? void 0 : _a.pricingPhases[0];
}
/**
* Returns true if the product can be purchased.
*/
get canPurchase() {
// Pseudo implementation to make typescript happy.
// see Object.defineProperty in the constructor for the actual implementation.
return false;
}
/**
* Returns true if the product is owned.
*
* Important: This value will be false when the app starts and will only become
* true after purchase receipts have been loaded and validated. Without receipt validation,
* it might remain false depending on the platform, make sure to store the ownership status
* of non-consumable products in some way.
*/
get owned() {
// Pseudo implementation to make typescript happy.
// see Object.defineProperty in the constructor for the actual implementation.
return false;
}
/**
* Find and return an offer for this product from its id
*
* If id isn't specified, returns the first offer.
*
* @param id - Identifier of the offer to return
* @return An Offer or undefined if no match is found
*/
getOffer(id = '') {
if (!id)
return this.offers[0];
return this.offers.find(o => o.id === id);
}
/**
* Add an offer to this product.
*
* @internal
*/
addOffer(offer) {
if (this.getOffer(offer.id))
return this;
this.offers.push(offer);
return this;
}
}
CdvPurchase.Product = Product;
})(CdvPurchase || (CdvPurchase = {}));
// Functions defined here so we can generate code compatible with old version of JS
var CdvPurchase;
(function (CdvPurchase) {
let Utils;
(function (Utils) {
/** Object.values() for ES6 */
function objectValues(obj) {
const ret = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret.push(obj[key]);
}
}
return ret;
}
Utils.objectValues = objectValues;
})(Utils = CdvPurchase.Utils || (CdvPurchase.Utils = {}));
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
let Utils;
(function (Utils) {
/** Returns human format name for a given platform */
function platformName(platform) {
switch (platform) {
case CdvPurchase.Platform.APPLE_APPSTORE:
return "App Store";
case CdvPurchase.Platform.GOOGLE_PLAY:
return "Google Play";
case CdvPurchase.Platform.WINDOWS_STORE:
return "Windows Store";
case CdvPurchase.Platform.BRAINTREE:
return "Braintree";
case CdvPurchase.Platform.TEST:
return "Test";
default: return platform;
}
}
Utils.platformName = platformName;
})(Utils = CdvPurchase.Utils || (CdvPurchase.Utils = {}));
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
/**
* @internal
*/
let Internal;
(function (Internal) {
/** Queue of receipts to validate */
class ReceiptsToValidate {
constructor() {
this.array = [];
}
get length() {
return this.array.length;
}
get() {
return this.array.concat();
}
add(receipt) {
if (!this.has(receipt))
this.array.push(receipt);
}
clear() {
while (this.array.length !== 0)
this.array.pop();
}
has(receipt) {
return !!this.array.find(el => el === receipt);
}
}
Internal.ReceiptsToValidate = ReceiptsToValidate;
/** Handles communication with the remote receipt validation service */
class Validator {
constructor(controller, log) {
/** List of receipts waiting for validation */
this.receiptsToValidate = new ReceiptsToValidate();
/** List of verified receipts */
this.verifiedReceipts = [];
this.numRequests = 0;
this.numResponses = 0;
/**
* For each md5-hashed values of the validator request's ".transaction" field,
* store the response from the server.
*
* This way, if a subsequent request is necessary (without a couple of minutes)
* we just reuse the same data.
*/
this.cache = {};
this.controller = controller;
this.log = log.child('Validator');
}
incrRequestsCounter() {
this.numRequests = (this.numRequests + 1) | 0;
this.log.debug(`Validation requests=${this.numRequests} responses=${this.numResponses}`);
}
incrResponsesCounter() {
this.numResponses = (this.numResponses + 1) | 0;
this.log.debug(`Validation requests=${this.numRequests} responses=${this.numResponses}`);
}
/** Add/update a verified receipt from the server response */
addVerifiedReceipt(receipt, data) {
for (const vr of this.verifiedReceipts) {
if (vr.platform === receipt.platform && vr.id === data.id) {
// update existing receipt
this.log.debug("Updating existing receipt.");
vr.set(receipt, data);
return vr;
}
}
this.log.debug("Register a new verified receipt.");
const newVR = new CdvPurchase.VerifiedReceipt(receipt, data, this.controller);
this.verifiedReceipts.push(newVR);
return newVR;
}
/** Add a receipt to the validation queue. It'll get validated after a few milliseconds. */
add(receiptOrTransaction) {
this.log.debug("Schedule validation: " + JSON.stringify(receiptOrTransaction));
const receipt = (receiptOrTransaction instanceof CdvPurchase.Transaction) ? receiptOrTransaction.parentReceipt : receiptOrTransaction;
if (!this.receiptsToValidate.has(receipt)) {
this.incrRequestsCounter();
this.receiptsToValidate.add(receipt);
}
}
/** Run validation for all receipts in the queue */
run() {
// pseudo implementation
const receipts = this.receiptsToValidate.get();
this.receiptsToValidate.clear();
const onResponse = (r) => __awaiter(this, void 0, void 0, function* () {
var _a;
const { receipt, payload } = r;
this.incrResponsesCounter();
try {
const adapter = this.controller.adapters.find(receipt.platform);
yield (adapter === null || adapter === void 0 ? void 0 : adapter.handleReceiptValidationResponse(receipt, payload));
if (payload.ok) {
const vr = this.addVerifiedReceipt(receipt, payload.data);
this.controller.verifiedCallbacks.trigger(vr, 'payload_ok');
// this.verifiedCallbacks.trigger(data.receipt);
}
else if (payload.code === CdvPurchase.ErrorCode.VALIDATOR_SUBSCRIPTION_EXPIRED) {
// find the subscription in an existing verified receipt and mark as expired.
const transactionId = (_a = receipt.lastTransaction()) === null || _a === void 0 ? void 0 : _a.transactionId;
const vr = transactionId ? this.verifiedReceipts.find(r => { var _a; return ((_a = r.collection[0]) === null || _a === void 0 ? void 0 : _a.transactionId) === transactionId; }) : undefined;
if (vr) {
vr === null || vr === void 0 ? void 0 : vr.collection.forEach(col => {
if (col.transactionId === transactionId)
col.isExpired = true;
});
this.controller.verifiedCallbacks.trigger(vr, 'payload_expired');
}
else {
this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'no_verified_receipt');
}
}
else {
this.controller.unverifiedCallbacks.trigger({ receipt, payload }, 'validator_error');
}
}
catch (err) {
this.log.error('Exception probably caused by an invalid response from the validator.' + err.message);
this.controller.unverifiedCallbacks.trigger({ receipt, payload: {
ok: false,
code: CdvPurchase.ErrorCode.VERIFICATION_FAILED,
message: err.message,
} }, 'validator_exception');
}
});
receipts.forEach(receipt => this.runOnReceipt(receipt, onResponse));
}
runOnReceipt(receipt, callback) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (receipt.platform === CdvPurchase.Platform.TEST) {
this.log.debug('Using Test Adapter mock verify function.');
return CdvPurchase.Test.Adapter.verify(receipt, callback);
}
if (!this.controller.validator) {
this.incrResponsesCounter();
// for backward compatibility, we consider that the receipt is verified.
callback({
receipt,
payload: {
ok: true,
data: {
id: ((_b = (_a = receipt.transactions) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.transactionId) || 'unknown',
latest_receipt: true,
transaction: { type: 'test' } // dummy data
}
}
});
return;
}
const body = yield this.buildRequestBody(receipt);
if (!body) {
this.incrResponsesCounter();
return;
}
if (typeof this.controller.validator === 'function')
return this.runValidatorFunction(this.controller.validator, receipt, body, callback);
const target = typeof this.controller.validator === 'string'
? {
url: this.controller.validator,
timeout: 20000, // validation request will timeout after 20 seconds by default
}
: this.controller.validator;
return this.runValidatorRequest(target, receipt, body, callback);
});
}
runValidatorFunction(validator, receipt, body, callback) {
try {
validator(body, (payload) => callback({ receipt, payload }));
}
catch (error) {
this.log.warn("user provided validator function failed with error: " + (error === null || error === void 0 ? void 0 : error.stack));
}
}
buildRequestBody(receipt) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
// Let the adapter generate the initial content
const adapter = this.controller.adapters.find(receipt.platform);
const body = yield (adapter === null || adapter === void 0 ? void 0 : adapter.receiptValidationBody(receipt));
if (!body)
return;
// Add the applicationUsername
body.additionalData = Object.assign(Object.assign({}, (_a = body.additionalData) !== null && _a !== void 0 ? _a : {}), { applicationUsername: this.controller.getApplicationUsername() });
if (!body.additionalData.applicationUsername)
delete body.additionalData.applicationUsername;
// Add device information
body.device = Object.assign(Object.assign({}, (_b = body.device) !== null && _b !== void 0 ? _b : {}), CdvPurchase.Validator.Internal.getDeviceInfo(this.controller));
// Add legacy pricing information
if (((_c = body.offers) === null || _c === void 0 ? void 0 : _c.length) === 1) {
const offer = body.offers[0];
if (offer.pricingPhases.length === 1) {
const pricing = offer.pricingPhases[0];
body.currency = pricing.currency;
body.priceMicros = pricing.priceMicros;
}
else if (offer.pricingPhases.length === 2) {
const pricing = offer.pricingPhases[1];
body.currency = pricing.currency;
body.priceMicros = pricing.priceMicros;
const intro = offer.pricingPhases[0];
body.introPriceMicros = intro.priceMicros;
}
}
return body;
});
}
removeExpiredCache() {
const now = +new Date();
const deleteList = [];
for (const hash in this.cache) {
if (this.cache[hash].expires < now) {
deleteList.push(hash);
}
}
for (const hash of deleteList) {
delete this.cache[hash];
}
}
runValidatorRequest(target, receipt, body, callback) {
this.removeExpiredCache();
const bodyTransactionHash = CdvPurchase.Utils.md5(JSON.stringify(body.transaction));
const cached = this.cache[bodyTransactionHash];
if (cached) {
this.log.debug("validator cache hit, using cached response");
return callback({ receipt, payload: cached.payload });
}
CdvPurchase.Utils.ajax(this.log.child("Ajax"), {
url: target.url,
method: 'POST',
customHeaders: target.headers,
timeout: target.timeout,
data: body,
success: (response) => {
var _a;
this.log.debug("validator success, response: " + JSON.stringify(response));
if (!isValidatorResponsePayload(response))
return callback({
receipt,
payload: {
ok: false,
code: CdvPurchase.ErrorCode.BAD_RESPONSE,
message: 'Validator responded with invalid data',
data: { latest_receipt: (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.latest_receipt },
}
});
this.cache[bodyTransactionHash] = {
payload: response,
expires: (+new Date()) + 120000, // expires in 2 minutes
};
callback({ receipt, payload: response });
},
error: (status, message, data) => {
var fullMessage = "Error " + status + ": " + message;
this.log.debug("validator failed, response: " + JSON.stringify(fullMessage));
this.log.debug("body => " + JSON.stringify(data));
callback({
receipt,
payload: {
ok: false,
message: fullMessage,
code: CdvPurchase.ErrorCode.COMMUNICATION,
status: status,
data: {},
}
});
}
});
}
}
Internal.Validator = Validator;
/**
* Check if a payload looks like a valid validator response.
*/
function isValidatorResponsePayload(payload) {
// TODO: could be made more robust.
return (!!payload)
&& (typeof payload === 'object')
&& ('ok' in payload)
&& (typeof payload.ok === 'boolean');
}
})(Internal = CdvPurchase.Internal || (CdvPurchase.Internal = {}));
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
/** @internal */
let Internal;
(function (Internal) {
/**
* The list of active platform adapters
*/
class Adapters {
constructor() {
/**
* List of instantiated adapters.
*
* They are added to this list by "initialize()".
*/
this.list = [];
}
add(log, adapters, context) {
adapters.forEach(po => {
log.info("");
if (this.find(po.platform))
return;
switch (po.platform) {
case CdvPurchase.Platform.APPLE_APPSTORE:
return this.list.push(new CdvPurchase.AppleAppStore.Adapter(context, po.options || {}));
case CdvPurchase.Platform.GOOGLE_PLAY:
return this.list.push(new CdvPurchase.GooglePlay.Adapter(context));
case CdvPurchase.Platform.BRAINTREE:
if (!po.options) {
log.error('Options missing for Braintree initialization. Use {platform: Platform.BRAINTREE, options: {...}} in your call to store.initialize');
}
return this.list.push(new CdvPurchase.Braintree.Adapter(context, po.options));
case CdvPurchase.Platform.TEST:
return this.list.push(new CdvPurchase.Test.Adapter(context));
default:
return;
}
});
}
/**
* Initialize some platform adapters.
*/
initialize(platforms, context) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof platforms === 'string') {
platforms = [platforms];
}
const newPlatforms = platforms.map(p => typeof p === 'string' ? { platform: p } : p).filter(p => !this.find(p.platform));
const log = context.log.child('Adapters');
log.info("Adding platforms: " + JSON.stringify(newPlatforms));
this.add(log, newPlatforms, context);
const products = context.registeredProducts.byPlatform();
const result = yield Promise.all(newPlatforms.map((platformToInit) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const platformProducts = (_c = (_b = (_a = products.filter(p => p.platform === platformToInit.platform)) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.products) !== null && _c !== void 0 ? _c : [];
const adapter = this.find(platformToInit.platform);
if (!adapter)
return;
log.info(`${adapter.name} initializing...`);
if (!adapter.isSupported) {
log.info(`${adapter.name} is not supported.`);
return; // skip unsupported adapters
}
const initResult = yield adapter.initialize();
adapter.ready = true;
log.info(`${adapter.name} initialized. ${initResult ? JSON.stringify(initResult) : ''}`);
if (initResult === null || initResult === void 0 ? void 0 : initResult.code)
return initResult;
log.info(`${adapter.name} products: ${JSON.stringify(platformProducts)}`);
if (platformProducts.length === 0)
return;
let loadProductsResult = [];
let loadReceiptsResult = [];
if (adapter.supportsParallelLoading) {
[loadProductsResult, loadReceiptsResult] = yield Promise.all([
adapter.loadProducts(platformProducts),
adapter.loadReceipts()
]);
}
else {
loadProductsResult = yield adapter.loadProducts(platformProducts);
loadReceiptsResult = yield adapter.loadReceipts();
}
// const loadProductsResult = await adapter.loadProducts(platformProducts);
log.info(`${adapter.name} products loaded: ${JSON.stringify(loadProductsResult)}`);
const loadedProducts = loadProductsResult.filter(p => p instanceof CdvPurchase.Product);
context.listener.productsUpdated(platformToInit.platform, loadedProducts);
// const loadReceiptsResult = await adapter.loadReceipts();
log.info(`${adapter.name} receipts loaded: ${JSON.stringify(loadReceiptsResult)}`);
return loadProductsResult.filter(lr => 'code' in lr && 'message' in lr)[0];
})));
return result.filter(err => err);
});
}
/**
* Retrieve a platform adapter.
*/
find(platform) {
return this.list.filter(a => a.id === platform)[0];
}
/**
* Retrieve the first platform adapter in the ready state, if any.
*
* You can optionally force the platform adapter you are looking for.
*
* Useful for methods that accept an optional "platform" argument, so they either act
* on the only active adapter or on the one selected by the user, if it's ready.
*/
findReady(platform) {
return this.list.filter(adapter => (!platform || adapter.id === platform) && adapter.ready)[0];
}
}
Internal.Adapters = Adapters;
})(Internal = CdvPurchase.Internal || (CdvPurchase.Internal = {}));
})(CdvPurchase || (CdvPurchase = {}));
var CdvPurchase;
(function (CdvPurchase) {
let Internal;
(function (Internal) {
/**
* Monitor the updates for products and receipt.
*
* Call the callbacks when appropriate.
*/
class StoreAdapterListener {
constructor(delegate, log) {
/** The list of supported platforms, needs to be set by "store.initialize" */
this.supportedPlatforms = [];
/** Those platforms have reported that their receipts are ready */
this.platformWithReceiptsReady = [];
this.lastTransactionState = {};
/** Store the listener's latest calling time (in ms) for a given transaction at a given state */
this.lastCallTimeForState = {};
this.updatedReceiptsToProcess = [];
this.delegate = delegate;
this.log = log.child('AdapterListener');
}
static makeTransactionToken(transaction) {
return transaction.platform + '|' + transaction.transactionId;
}
/**
* Set the list of supported platforms.
*
* Called by the store when it is initialized.
*/
setSupportedPlatforms(platforms) {
this.log.debug(`setSupportedPlatforms: ${platforms.join(',')} (${this.platformWithReceiptsReady.length} have their receipts ready)`);
this.supportedPlatforms = platforms;
if (this.supportedPlatforms.length === this.platformWithReceiptsReady.length) {
this.log.debug('triggering receiptsReady()');
this.delegate.receiptsReadyCallbacks.trigger(undefined, 'adapterListener_setSupportedPlatforms');
}
}
/**
* Trigger the "receiptsReady" event when all platforms have reported that their receipts are ready.
*
* This function is used by adapters to report that their receipts are ready.
* Once all adapters have reported their receipts, the "receiptsReady" event is triggered.
*
* @param platform The platform that has its receipts ready.
*/
receiptsReady(platform) {
if (this.supportedPlatforms.length > 0 && this.platformWithReceiptsReady.length === this.supportedPlatforms.length) {
this.log.debug('receiptsReady: ' + platform + '(skipping)');
return;
}
if (this.platformWithReceiptsReady.indexOf(platform) < 0) {
this.platformWithReceiptsReady.push(platform);
this.log.debug(`receiptsReady: ${platform} (${this.platformWithReceiptsReady.length}/${this.supportedPlatforms.length})`);
if (this.platformWithReceiptsReady.length === this.supportedPlatforms.length) {
this.log.debug('triggering receiptsReady()');
this.delegate.receiptsReadyCallbacks.trigger(undefined, 'adapterListener_receiptsReady');
}
}
}
/**
* Trigger the "updated" event for each product.
*/
productsUpdated(platform, products) {
products.forEach(product => this.delegate.updatedCallbacks.trigger(product, 'adapterListener_productsUpdated'));
}
/**
* Triggers the "approved", "pending" and "finished" events for transactions.
*
* - "approved" is triggered only if it hasn't been called for the same transaction in the last 5 seconds.
* - "finished" and "pending" are triggered only if the transaction state has changed.
*
* @param platform The platform that has its receipts updated.
* @param receipts The receipts that have been updated.
*/
receiptsUpdated(platform, receipts) {
this.log.debug("receiptsUpdated: " + JSON.stringify(receipts.map(r => ({
platform: r.platform,
transactions: r.transactions,
}))));
for (const receipt of receipts) {
if (this.updatedReceiptsToProcess.indexOf(receipt) < 0) {
this.updatedReceiptsToProcess.push(receipt);
}
}
if (this.updatedReceiptsProcessor !== undefined) {
clearTimeout(this.updatedReceiptsProcessor);
}
this.updatedReceiptsProcessor = setTimeout(() => {
this._processUpdatedReceipts();
}, 500);
}
_processUpdatedReceipts() {
this.log.debug("processing " + this.updatedReceiptsToProcess.length + " updated receipts");
const now = +new Date();
const receipts = this.updatedReceiptsToProcess;
this.updatedReceiptsToProcess = [];
receipts.forEach(receipt => {
this.delegate.updatedReceiptCallbacks.trigger(receipt, 'adapterListener_receiptsUpdated');
receipt.transactions.forEach(transaction => {
var _a;
const transactionToken = StoreAdapterListener.makeTransactionToken(transaction);
const tokenWithState = transactionToken + '@' + transaction.state;
const lastState = this.lastTransactionState[transactionToken];
// Retrigger "approved", so validation is rerun on potential update.
if (transaction.state === CdvPurchase.TransactionState.APPROVED) {
// prevent calling approved twice in a very short period (60 seconds).
const lastCalled = (_a = this.lastCallTimeForState[tokenWithState]) !== null && _a !== void 0 ? _a : 0;
if (now - lastCalled > 60000) {
this.lastCallTimeForState[tokenWithState] = now;
this.delegate.approvedCallbacks.trigger(transaction, 'adapterListener_receiptsUpdated_approved');
}
else {
this.log.debug(`Skipping ${tokenWithState}, because it has been last called ${lastCalled > 0 ? Math.round(now - lastCalled) + 'ms ago (' + now + '-' + lastCalled + ')' : 'never'}`);
}
}