UNPKG

booletwa

Version:

Generate TWA projects from a Web Manifest

242 lines (217 loc) 8.8 kB
/* * Copyright 2021 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {androidpublisher_v3 as androidPublisher, google} from 'googleapis'; import {createReadStream} from 'fs'; // Possible values for release tracks const TRACK_VALUES = ['alpha', 'beta', 'internal', 'production']; export type PlayStoreTrack = typeof TRACK_VALUES[number]; export const PlayStoreTracks: PlayStoreTrack[] = [...TRACK_VALUES]; const PLAY_API_TIMEOUT_MS = 180000; export function asPlayStoreTrack(input?: string): PlayStoreTrack | null { if (!input) { return null; } return TRACK_VALUES.includes(input) ? input as PlayStoreTrack : null; } export class GooglePlay { private _googlePlayApi: androidPublisher.Androidpublisher; /** * Constructs a Google Play object with the gradleWrapper so we can use a * gradle plugin to communicate with Google Play. * * @param serviceAccountJsonFilePath This is the service account file to communicate with the * play publisher API. */ constructor(serviceAccountJsonFilePath: string) { this._googlePlayApi = this.getAndroidClient(serviceAccountJsonFilePath); if (!this._googlePlayApi) { throw new Error('Could not create a Google Play API client'); } } /** * Generates an editId that should be used in all play operations. * @param packageName - The package that will be worked upon. * @param operation - The Play Operation which requires an editId injected * @param commitEdit - Whether or not this edit is a action or query edit. true indicates an action edit. * @returns - A PlayOperationResult which contains the output of the operation in a field that was worked upon. */ async performPlayOperation<Type>(packageName: string, operation: (editId: string) => Promise<Type>, commitEdit = false): Promise<Type> { const editId = await this.startPlayOperation(packageName); const result = await operation(editId); if (!commitEdit) { await this.endPlayOperation(packageName, editId); } return result; } /** * This calls the publish bundle command and publishes an existing artifact to Google * Play. * Calls the following Play API commands in order: * Edits.Insert * Edits.Bundles.Upload * Edits.Tracks.Update * Edits.Commit * https://developers.google.com/android-publisher/api-ref/rest/v3/edits.bundles/upload * https://developers.google.com/android-publisher/edits#workflow * @param track - Specifies the track that the user would like to publish to. * @param filepath - Filepath of the App bundle you would like to upload. * @param packageName - Package name of the bundle. * @param retainedBundles - All bundles that should be retained on upload. This is useful for * ChromeOS only releases. */ async publishBundle( track: PlayStoreTrack, filepath: string, packageName: string, retainedBundles: number[], editId: string, ): Promise<void> { const result = await this._googlePlayApi.edits.bundles.upload( { ackBundleInstallationWarning: false, editId: editId, packageName: packageName, media: { body: createReadStream(filepath), }, }, { timeout: PLAY_API_TIMEOUT_MS, }); const versionCodeUploaded = result.data.versionCode; if (!versionCodeUploaded) { throw new Error('Version code could not be found from Play API.'); } const retainedBundlesStr = retainedBundles.map((n) => n.toString()); await this.addBundleToTrack( track, [versionCodeUploaded.toString(), ...retainedBundlesStr], packageName, editId, ); await this._googlePlayApi.edits.commit( { changesNotSentForReview: false, editId: editId, packageName: packageName, }, ); } /** * This calls the Edits.Tracks.Update play publisher api command. This will do the updating of * the user selected track for the reelase to the play store. * @param track - Specifies the track that the user would like to publish to. * @param versionCodes - Specifies all versions of the app bundle to be included on release * (including retained artifacts). * @param packageName - packageName of the bundle. * @param editId - The current edit hosted on Google Play. */ private async addBundleToTrack( track: PlayStoreTrack, versionCodes: string[], packageName: string, editId: string): Promise<void> { const tracksUpdate: androidPublisher.Params$Resource$Edits$Tracks$Update = { track: track, packageName: packageName, editId: editId, requestBody: { releases: [{ versionCodes: versionCodes, status: 'completed', }], track: track, }, }; await this._googlePlayApi.edits.tracks.update(tracksUpdate); } /** * Connects to the Google Play Console and retrieves a list of all Android App Bundles for the * given packageName. Finds the largest versionCode of those bundles and returns it. Considers * both ChromeOS and Android Releases. * @param packageName - The packageName of the versionCode we are looking up. */ async getLargestVersionCode(packageName: string, editId: string): Promise<number> { const bundleResponse = await this._googlePlayApi.edits.bundles.list({packageName: packageName, editId: editId}); if (!bundleResponse.data.bundles) { throw new Error('No bundles found from Google Play'); } const versionCode = Math.max( ...bundleResponse.data.bundles.map((bundle) => bundle.versionCode!!)); return versionCode; } /** * Starts an edit on the Play servers. This is the basis for any play publishing api operation. * @param packageName - the packageName of the app we want to interact with. */ private async startPlayOperation(packageName: string): Promise<string> { const edit = await this._googlePlayApi.edits.insert({packageName: packageName}); const editId = edit.data.id; if (!editId) { throw new Error('Could not create a Google Play edit'); } return editId; } /** * Cancels the edit in progress on the Play server. * @param packageName - The packageName of the app we are interacting with. * @param editId - The editId that is currently in progress. */ private async endPlayOperation(packageName: string, editId: string): Promise<void> { await this._googlePlayApi.edits.delete({editId: editId, packageName: packageName}); } /** * Checks to see if the version that we want to retain already exists within the Play Store. * @param packageName - The packageName of the versionCode we are looking up. * @param versionCode - The version code of the APK / Bundle we want to retain. */ async versionExists(packageName: string, versionCode: number, editId: string): Promise<boolean> { const uploadedApks = await this._googlePlayApi.edits.apks.list({packageName: packageName, editId: editId}); let found = uploadedApks.data.apks?.find((obj) => obj.versionCode == versionCode); if (found) { return true; } const uploadedBundles = await this._googlePlayApi.edits.bundles.list({packageName: packageName, editId: editId}); found = uploadedBundles.data.bundles?.find((obj) => obj.versionCode == versionCode); if (found) { return true; } return false; } /** * This fetches the Android client using the bubblewrap configuration file. * @param serviceAccountJsonFilePath - The file path to the service account file. This allows * communication to the Play Publisher API. */ private getAndroidClient(serviceAccountJsonFilePath: string): androidPublisher.Androidpublisher { // Initialize the Google API Client from service account credentials const jwtClient = new google.auth.JWT({ keyFile: serviceAccountJsonFilePath, scopes: ['https://www.googleapis.com/auth/androidpublisher'], }); // Connect to the Google Play Developer API with JWT Client return google.androidpublisher({ version: 'v3', auth: jwtClient, }); } }