Skip to content

Commit 3ef3f23

Browse files
feat(auth): initial Apple IAP modules
Because: * We want SubPlat to know about Apple IAP subscriptions for our RPs. This commit: * Sets up the scaffolding for the needed modules including base classes, their methods and types. Closes #10313
1 parent 5fc637e commit 3ef3f23

File tree

10 files changed

+630
-3
lines changed

10 files changed

+630
-3
lines changed

packages/fxa-auth-server/config/index.ts

+22
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,28 @@ const conf = convict({
780780
env: 'PAYPAL_NVP_SIGNATURE',
781781
},
782782
},
783+
appStore: {
784+
credentials: {
785+
doc: 'Map of AppStore Connect credentials by app bundle ID',
786+
format: Object,
787+
default: {
788+
// Cannot use an actual bundleId (e.g. 'org.mozilla.ios.FirefoxVPN') as the key
789+
// due to https://github.com/mozilla/node-convict/issues/250
790+
org_mozilla_ios_FirefoxVPN: {
791+
issuerId: 'issuer_id',
792+
serverApiKey: 'key',
793+
serverApiKeyId: 'key_id',
794+
},
795+
},
796+
env: 'APP_STORE_CREDENTIALS',
797+
},
798+
sandbox: {
799+
doc: 'Apple App Store Sandbox mode',
800+
format: Boolean,
801+
env: 'APP_STORE_SANDBOX',
802+
default: true,
803+
},
804+
},
783805
playApiServiceAccount: {
784806
credentials: {
785807
client_email: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
import { Firestore } from '@google-cloud/firestore';
5+
import { AppStoreServerAPI, Environment } from 'app-store-server-api';
6+
import { Container } from 'typedi';
7+
import { TypedCollectionReference } from 'typesafe-node-firestore';
8+
9+
import { AppConfig, AuthFirestore, AuthLogger } from '../../types';
10+
// TODO: promote this to a shared dir
11+
import { IapConfig } from '../google-play/types';
12+
import { PurchaseManager } from './purchase-manager';
13+
14+
export class AppleIAP {
15+
private firestore: Firestore;
16+
private log: AuthLogger;
17+
private prefix: string;
18+
private iapConfigDbRef: TypedCollectionReference<IapConfig>;
19+
20+
public purchaseManager: PurchaseManager;
21+
constructor() {
22+
const {
23+
authFirestore,
24+
env,
25+
subscriptions: { appStore },
26+
} = Container.get(AppConfig);
27+
this.prefix = `${authFirestore.prefix}iap-`;
28+
this.firestore = Container.get(AuthFirestore);
29+
this.iapConfigDbRef = this.firestore.collection(
30+
`${this.prefix}iap-config`
31+
) as TypedCollectionReference<IapConfig>;
32+
this.log = Container.get(AuthLogger);
33+
34+
// Initialize App Store Server API client per bundle ID
35+
const environment =
36+
env === 'prod' ? Environment.Production : Environment.Sandbox;
37+
const appStoreServerApiClients = {};
38+
for (const [bundleIdWithUnderscores, credentials] of Object.entries(
39+
appStore.credentials
40+
)) {
41+
// Cannot use an actual bundleId (e.g. 'org.mozilla.ios.FirefoxVPN') as the key
42+
// due to https://github.com/mozilla/node-convict/issues/250
43+
const bundleId = bundleIdWithUnderscores.replace('_', '.');
44+
const { serverApiKey, serverApiKeyId, issuerId } = credentials;
45+
appStoreServerApiClients[bundleId] = new AppStoreServerAPI(
46+
serverApiKey,
47+
serverApiKeyId,
48+
issuerId,
49+
bundleId,
50+
environment
51+
);
52+
}
53+
const purchasesDbRef = this.firestore.collection(
54+
`${this.prefix}app-store-purchases`
55+
);
56+
this.purchaseManager = new PurchaseManager(
57+
purchasesDbRef,
58+
appStoreServerApiClients
59+
);
60+
}
61+
62+
/**
63+
* Fetch the Apple plans for iOS client usage.
64+
*/
65+
public async plans(appName: string) {
66+
const doc = await this.iapConfigDbRef.doc(appName).get();
67+
if (doc.exists) {
68+
return doc.data()?.plans;
69+
} else {
70+
throw Error(`IAP Plans document does not exist for ${appName}`);
71+
}
72+
}
73+
74+
/**
75+
* Fetch the App Store bundleId for the given appName.
76+
*/
77+
public async getBundleId(appName: string) {
78+
const doc = await this.iapConfigDbRef.doc(appName).get();
79+
if (doc.exists) {
80+
return doc.data()?.bundleId;
81+
} else {
82+
throw Error(`IAP Plans document does not exist for ${appName}`);
83+
}
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Error Codes representing an error that is temporary to Apple
3+
* and should be retried again without changes.
4+
* https://developer.apple.com/documentation/appstoreserverapi/error_codes
5+
*/
6+
export const APP_STORE_RETRY_ERRORS = [4040002, 4040004, 5000001, 4040006];
7+
8+
export class AppStoreRetryableError extends Error {
9+
public errorCode: number;
10+
public errorMessage: string;
11+
12+
constructor(errorCode: number, errorMessage: string, ...params: any) {
13+
super(...params);
14+
this.name = 'AppStoreRetryableError';
15+
this.errorCode = errorCode;
16+
this.message = errorMessage;
17+
}
18+
}

0 commit comments

Comments
 (0)