mirror of
https://github.com/mozilla/fxa.git
synced 2025-12-13 20:36:41 +01:00
fix(iap): handle invalid google play store purchaseTokens
This commit is contained in:
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -21,13 +21,13 @@
|
||||
"protocol": "inspector",
|
||||
"env": {
|
||||
"DEBUG": "1",
|
||||
"NODE_OPTIONS": "--dns-result-order=ipv4first",
|
||||
"NODE_OPTIONS": "--dns-result-order=ipv4first"
|
||||
},
|
||||
"program": "${workspaceFolder}/node_modules/@playwright/test/cli.js",
|
||||
"args": ["test","--config=${workspaceFolder}/packages/functional-tests/playwright.config.ts", "--project=local"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"cwd":"${workspaceFolder}/packages/functional-tests",
|
||||
"request": "launch",
|
||||
"request": "launch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('GoogleIapPurchaseManager', () => {
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(googleIapClient, 'getSubscriptions')
|
||||
.spyOn(googleIapClient, 'getSubscription')
|
||||
.mockResolvedValue(mockApiResponse);
|
||||
|
||||
jest.spyOn(repository, 'getPurchase').mockResolvedValue(undefined);
|
||||
@@ -114,7 +114,7 @@ describe('GoogleIapPurchaseManager', () => {
|
||||
|
||||
const result = await manager.getFromPlayStoreApi(packageName, sku, token);
|
||||
|
||||
expect(googleIapClient.getSubscriptions).toHaveBeenCalledWith(
|
||||
expect(googleIapClient.getSubscription).toHaveBeenCalledWith(
|
||||
packageName,
|
||||
sku,
|
||||
token
|
||||
@@ -124,7 +124,7 @@ describe('GoogleIapPurchaseManager', () => {
|
||||
|
||||
it('throws wrapped error when Firestore fails', async () => {
|
||||
const token = faker.string.uuid();
|
||||
jest.spyOn(googleIapClient, 'getSubscriptions').mockResolvedValue({});
|
||||
jest.spyOn(googleIapClient, 'getSubscription').mockResolvedValue({});
|
||||
jest.spyOn(repository, 'getPurchase').mockImplementation(() => {
|
||||
throw new Error('Firestore failure');
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from './google-iap-purchase.repository';
|
||||
import type { NotificationType } from './types';
|
||||
import { GoogleIapClient } from './google-iap.client';
|
||||
import { GoogleIapGetFromPlayStoreUnknownError } from './google-iap.error';
|
||||
import { GoogleIapGetFromPlayStoreUnknownError, GoogleIapSubscriptionNotFoundError, GoogleIapSubscriptionPurchaseTokenInvalidError } from './google-iap.error';
|
||||
import { REPLACED_PURCHASE_USERID_PLACEHOLDER } from './constants';
|
||||
|
||||
@Injectable()
|
||||
@@ -66,11 +66,25 @@ export class GoogleIapPurchaseManager {
|
||||
this.log.debug('queryCurrentSubscriptions.cache.update', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
purchase = await this.getFromPlayStoreApi(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
try {
|
||||
purchase = await this.getFromPlayStoreApi(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
} catch(e) {
|
||||
if (e instanceof GoogleIapSubscriptionNotFoundError) {
|
||||
this.log.error('queryCurrentSubscriptions.purchaseTokenNotFound', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
} else if (e instanceof GoogleIapSubscriptionPurchaseTokenInvalidError) {
|
||||
this.log.error('queryCurrentSubscriptions.invalidPurchaseToken', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (purchase.isAccountHold() || purchase.isPaused()) {
|
||||
@@ -103,7 +117,7 @@ export class GoogleIapPurchaseManager {
|
||||
triggerNotificationType?: NotificationType
|
||||
): Promise<PlayStoreSubscriptionPurchase> {
|
||||
// STEP 1. Query Play Developer API to verify the purchase token
|
||||
const apiResponse = await this.googleIapClient.getSubscriptions(
|
||||
const apiResponse = await this.googleIapClient.getSubscription(
|
||||
packageName,
|
||||
sku,
|
||||
purchaseToken
|
||||
@@ -209,7 +223,7 @@ export class GoogleIapPurchaseManager {
|
||||
// Purchase record not found in Firestore. We'll try to fetch purchase detail from Play Developer API to backfill the missing cache
|
||||
|
||||
const apiResponse = await this.googleIapClient
|
||||
.getSubscriptions(packageName, sku, purchaseToken)
|
||||
.getSubscription(packageName, sku, purchaseToken)
|
||||
.catch((err) => {
|
||||
// We only log an warning to as there is chance that backfilling is impossible.
|
||||
// For example: after a subscription upgrade, the new token has linkedPurchaseToken to be the token before upgrade.
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('GoogleIapClient', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getSubscriptions', () => {
|
||||
describe('getSubscription', () => {
|
||||
it('should return subscription data', async () => {
|
||||
const mockPackageName = faker.string.uuid();
|
||||
const mockSku = faker.string.uuid();
|
||||
@@ -48,7 +48,7 @@ describe('GoogleIapClient', () => {
|
||||
)
|
||||
.mockResolvedValue({ data: mockResponseData });
|
||||
|
||||
const result = await googleIapClient.getSubscriptions(
|
||||
const result = await googleIapClient.getSubscription(
|
||||
mockPackageName,
|
||||
mockSku,
|
||||
mockPurchaseToken
|
||||
@@ -78,7 +78,7 @@ describe('GoogleIapClient', () => {
|
||||
.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
googleIapClient.getSubscriptions(
|
||||
googleIapClient.getSubscription(
|
||||
mockPackageName,
|
||||
mockSku,
|
||||
mockPurchaseToken
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GoogleIapClientUnexpectedTypeError,
|
||||
GoogleIapClientUnknownError,
|
||||
GoogleIapSubscriptionNotFoundError,
|
||||
GoogleIapSubscriptionPurchaseTokenInvalidError,
|
||||
} from './google-iap.error';
|
||||
|
||||
@Injectable()
|
||||
@@ -33,7 +34,7 @@ export class GoogleIapClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getSubscriptions(
|
||||
async getSubscription(
|
||||
packageName: string,
|
||||
sku: string,
|
||||
purchaseToken: string
|
||||
@@ -49,7 +50,10 @@ export class GoogleIapClient {
|
||||
return apiResponse.data;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && 'code' in e && e.code === 404) {
|
||||
throw new GoogleIapSubscriptionNotFoundError(packageName, sku, e);
|
||||
throw new GoogleIapSubscriptionNotFoundError(packageName, sku, purchaseToken, e);
|
||||
}
|
||||
if (e instanceof Error && 'code' in e && e.code === 410) {
|
||||
throw new GoogleIapSubscriptionPurchaseTokenInvalidError(packageName, sku, purchaseToken, e);
|
||||
}
|
||||
|
||||
throw this.convertError(e);
|
||||
|
||||
@@ -16,12 +16,19 @@ export class GoogleIapError extends BaseError {
|
||||
}
|
||||
|
||||
export class GoogleIapSubscriptionNotFoundError extends GoogleIapError {
|
||||
constructor(packageName: string, sku: string, cause: Error) {
|
||||
super('Google IAP subscription not found', { packageName, sku }, cause);
|
||||
constructor(packageName: string, sku: string, purchaseToken: string, cause: Error) {
|
||||
super('Google IAP subscription not found', { packageName, sku, purchaseToken }, cause);
|
||||
this.name = 'GoogleIapSubscriptionNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class GoogleIapSubscriptionPurchaseTokenInvalidError extends GoogleIapError {
|
||||
constructor(packageName: string, sku: string, purchaseToken: string, cause: Error) {
|
||||
super('Google IAP subscription purchase token invalid', { packageName, sku, purchaseToken }, cause);
|
||||
this.name = 'GoogleIapSubscriptionPurchaseTokenInvalidError';
|
||||
}
|
||||
}
|
||||
|
||||
export class GoogleIapInvalidMessagePayloadError extends GoogleIapError {
|
||||
constructor(messageData: string, cause: Error) {
|
||||
super('Invalid message payload', { messageData }, cause);
|
||||
|
||||
@@ -99,11 +99,21 @@ export class UserManager extends UserManagerBase {
|
||||
this.log.info('queryCurrentSubscriptions.cache.update', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
purchase = await this.purchaseManager.querySubscriptionPurchase(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
try {
|
||||
purchase = await this.purchaseManager.querySubscriptionPurchase(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
} catch(e) {
|
||||
if (e.name === PurchaseQueryError.INVALID_TOKEN) {
|
||||
this.log.error('queryCurrentSubscriptions.invalidPurchaseToken', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the updated purchase to list to returned to clients
|
||||
|
||||
@@ -223,7 +223,7 @@ export class PurchaseManager {
|
||||
|
||||
private convertPlayAPIErrorToLibraryError(playError: any): Error {
|
||||
const libraryError = new Error(playError.message);
|
||||
if (playError.code === 404) {
|
||||
if (playError.code === 404 || playError.code === 410) {
|
||||
libraryError.name = PurchaseQueryError.INVALID_TOKEN;
|
||||
} else {
|
||||
// Unexpected error occurred. It's likely an issue with Service Account
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
PlayStoreSubscriptionPurchase,
|
||||
} from './subscription-purchase';
|
||||
import { SkuType } from './types/purchases';
|
||||
import { PurchaseQueryError } from './types/errors';
|
||||
|
||||
export class UserManager {
|
||||
constructor(
|
||||
@@ -63,11 +64,21 @@ export class UserManager {
|
||||
this.log.info('queryCurrentSubscriptions.cache.update', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
purchase = await this.purchaseManager.querySubscriptionPurchase(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
try {
|
||||
purchase = await this.purchaseManager.querySubscriptionPurchase(
|
||||
purchase.packageName,
|
||||
purchase.sku,
|
||||
purchase.purchaseToken
|
||||
);
|
||||
} catch(e) {
|
||||
if (e.name === PurchaseQueryError.INVALID_TOKEN) {
|
||||
this.log.error('queryCurrentSubscriptions.invalidPurchaseToken', {
|
||||
purchaseToken: purchase.purchaseToken,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the updated purchase to list to returned to clients
|
||||
|
||||
Reference in New Issue
Block a user