Skip to content

Commit

Permalink
feat(auth): initial Apple IAP modules
Browse files Browse the repository at this point in the history
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
  • Loading branch information
biancadanforth committed Apr 13, 2022
1 parent 6ff2e3a commit 2b25ada
Show file tree
Hide file tree
Showing 10 changed files with 662 additions and 130 deletions.
20 changes: 20 additions & 0 deletions packages/fxa-auth-server/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,26 @@ const conf = convict({
env: 'PAYPAL_NVP_SIGNATURE',
},
},
appStore: {
credentials: {
doc: 'Map of AppStore Connect credentials by app bundle ID',
format: Object,
default: {
'org.mozilla.ios.FirefoxVPN': {
issuerId: 'issuer_id',
serverApiKey: 'key',
serverApiKeyId: 'key_id',
},
},
env: 'APP_STORE_CREDENTIALS',
},
sandbox: {
doc: 'Apple App Store Sandbox mode',
format: Boolean,
env: 'APP_STORE_SANDBOX',
default: true,
},
},
playApiServiceAccount: {
credentials: {
client_email: {
Expand Down
82 changes: 82 additions & 0 deletions packages/fxa-auth-server/lib/payments/apple-app-store/apple-iap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Firestore } from '@google-cloud/firestore';
import { AppStoreServerAPI, Environment } from 'app-store-server-api';
import { Container } from 'typedi';
import { TypedCollectionReference } from 'typesafe-node-firestore';

import { AppConfig, AuthFirestore, AuthLogger } from '../../types';
// TODO: promote this to a shared dir
import { IapConfig } from '../google-play/types';
import { PurchaseManager } from './purchase-manager';

export class AppleIAP {
private firestore: Firestore;
private log: AuthLogger;
private prefix: string;
private iapConfigDbRef: TypedCollectionReference<IapConfig>;

public purchaseManager: PurchaseManager;
constructor() {
const {
authFirestore,
env,
subscriptions: { appStore },
} = Container.get(AppConfig);
this.prefix = `${authFirestore.prefix}iap-`;
this.firestore = Container.get(AuthFirestore);
this.iapConfigDbRef = this.firestore.collection(
`${this.prefix}iap-config`
) as TypedCollectionReference<IapConfig>;
this.log = Container.get(AuthLogger);

// Initialize App Store Server API client per bundle ID
const environment =
env === 'prod' ? Environment.Production : Environment.Sandbox;
const appStoreServerApiClients = {};
for (const [bundleId, credentials] of Object.entries(
appStore.credentials
)) {
const { serverApiKey, serverApiKeyId, issuerId } = credentials;
appStoreServerApiClients[bundleId] = new AppStoreServerAPI(
serverApiKey,
serverApiKeyId,
issuerId,
bundleId,
environment
);
}
const purchasesDbRef = this.firestore.collection(
`${this.prefix}app-store-purchases`
);
this.purchaseManager = new PurchaseManager(
purchasesDbRef,
appStoreServerApiClients
);
}

/**
* Fetch the Apple plans for iOS client usage.
*/
public async plans(appName: string) {
const doc = await this.iapConfigDbRef.doc(appName).get();
if (doc.exists) {
return doc.data()?.plans;
} else {
throw Error(`IAP Plans document does not exist for ${appName}`);
}
}

/**
* Fetch the App Store bundleId for the given appName.
*/
public async getBundleId(appName: string) {
const doc = await this.iapConfigDbRef.doc(appName).get();
if (doc.exists) {
return doc.data()?.bundleId;
} else {
throw Error(`IAP Plans document does not exist for ${appName}`);
}
}
}
18 changes: 18 additions & 0 deletions packages/fxa-auth-server/lib/payments/apple-app-store/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Error Codes representing an error that is temporary to Apple
* and should be retried again without changes.
* https://developer.apple.com/documentation/appstoreserverapi/error_codes
*/
export const APP_STORE_RETRY_ERRORS = [4040002, 4040004, 5000001, 4040006];

export class AppStoreRetryableError extends Error {
public errorCode: number;
public errorMessage: string;

constructor(errorCode: number, errorMessage: string, ...params: any) {
super(...params);
this.name = 'AppStoreRetryableError';
this.errorCode = errorCode;
this.message = errorMessage;
}
}
Loading

0 comments on commit 2b25ada

Please sign in to comment.