-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fcmv1] Manage FCM V1 Google Service Account Key in CLI
# Why NOTE: DO NOT LAND THIS UNTIL GRAPHQL CHANGES LAND IN WWW We're adding support for FCM V1 credentials, as Google is shutting down the FCM Legacy API for sending Android notifications in June. # How Add new prompts and refactor existing prompts to match this schematic: # Test Plan Verified all functionality e2e:
- Loading branch information
1 parent
b5f2f52
commit eb9152a
Showing
18 changed files
with
585 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
33 changes: 33 additions & 0 deletions
33
packages/eas-cli/src/credentials/android/actions/AssignGoogleServiceAccountKeyForFcmV1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { | ||
CommonAndroidAppCredentialsFragment, | ||
GoogleServiceAccountKeyFragment, | ||
} from '../../../graphql/generated'; | ||
import Log from '../../../log'; | ||
import { CredentialsContext } from '../../context'; | ||
import { AppLookupParams } from '../api/GraphqlClient'; | ||
|
||
export class AssignGoogleServiceAccountKeyForFcmV1 { | ||
constructor(private app: AppLookupParams) {} | ||
|
||
public async runAsync( | ||
ctx: CredentialsContext, | ||
googleServiceAccountKey: GoogleServiceAccountKeyFragment | ||
): Promise<CommonAndroidAppCredentialsFragment> { | ||
const appCredentials = | ||
await ctx.android.createOrGetExistingAndroidAppCredentialsWithBuildCredentialsAsync( | ||
ctx.graphqlClient, | ||
this.app | ||
); | ||
const updatedAppCredentials = await ctx.android.updateAndroidAppCredentialsAsync( | ||
ctx.graphqlClient, | ||
appCredentials, | ||
{ | ||
googleServiceAccountKeyForFcmV1Id: googleServiceAccountKey.id, | ||
} | ||
); | ||
Log.succeed( | ||
`Google Service Account Key assigned to ${this.app.androidApplicationIdentifier} for FCM V1` | ||
); | ||
return updatedAppCredentials; | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
packages/eas-cli/src/credentials/android/actions/CreateGoogleServiceAccountKeyForFcmV1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import chalk from 'chalk'; | ||
import fs from 'fs-extra'; | ||
|
||
import { AccountFragment, GoogleServiceAccountKeyFragment } from '../../../graphql/generated'; | ||
import Log, { learnMore } from '../../../log'; | ||
import { promptAsync } from '../../../prompts'; | ||
import { CredentialsContext } from '../../context'; | ||
import { GoogleServiceAccountKey } from '../credentials'; | ||
import { | ||
detectGoogleServiceAccountKeyPathAsync, | ||
readAndValidateServiceAccountKey, | ||
} from '../utils/googleServiceAccountKey'; | ||
|
||
export class CreateGoogleServiceAccountKeyForFcmV1 { | ||
constructor(private account: AccountFragment) {} | ||
|
||
public async runAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKeyFragment> { | ||
if (ctx.nonInteractive) { | ||
throw new Error(`New FCM V1 Service Account Key cannot be created in non-interactive mode.`); | ||
} | ||
const jsonKeyObject = await this.provideAsync(ctx); | ||
const gsaKeyFragment = await ctx.android.createGoogleServiceAccountKeyAsync( | ||
ctx.graphqlClient, | ||
this.account, | ||
jsonKeyObject | ||
); | ||
Log.succeed('Uploaded FCM V1 Service Account Key.'); | ||
return gsaKeyFragment; | ||
} | ||
|
||
private async provideAsync(ctx: CredentialsContext): Promise<GoogleServiceAccountKey> { | ||
try { | ||
const keyJsonPath = await this.provideKeyJsonPathAsync(ctx); | ||
return readAndValidateServiceAccountKey(keyJsonPath); | ||
} catch (e) { | ||
Log.error(e); | ||
return await this.provideAsync(ctx); | ||
} | ||
} | ||
|
||
private async provideKeyJsonPathAsync(ctx: CredentialsContext): Promise<string> { | ||
const detectedPath = await detectGoogleServiceAccountKeyPathAsync(ctx.projectDir); | ||
if (detectedPath) { | ||
return detectedPath; | ||
} | ||
|
||
Log.log( | ||
`${chalk.bold( | ||
'A Google Service Account JSON key is required to send Android notifications via FCM V1' | ||
)}.\n` + | ||
`If you're not sure what this is or how to create one, ${learnMore( | ||
'https://expo.fyi/creating-google-service-account-for-fcm-v1', | ||
{ learnMoreMessage: 'learn more' } | ||
)}` | ||
); | ||
const { filePath } = await promptAsync({ | ||
name: 'filePath', | ||
message: 'Path to Google Service Account file:', | ||
initial: 'api-0000000000000000000-111111-aaaaaabbbbbb.json', | ||
type: 'text', | ||
// eslint-disable-next-line async-protect/async-suffix | ||
validate: async (filePath: string) => { | ||
try { | ||
const stats = await fs.stat(filePath); | ||
if (stats.isFile()) { | ||
return true; | ||
} | ||
return 'Input is not a file.'; | ||
} catch { | ||
return 'File does not exist.'; | ||
} | ||
}, | ||
}); | ||
return filePath; | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
packages/eas-cli/src/credentials/android/actions/SetUpGoogleServiceAccountKeyForFcmV1.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import nullthrows from 'nullthrows'; | ||
|
||
import { AssignGoogleServiceAccountKeyForFcmV1 } from './AssignGoogleServiceAccountKeyForFcmV1'; | ||
import { CreateGoogleServiceAccountKeyForFcmV1 } from './CreateGoogleServiceAccountKeyForFcmV1'; | ||
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey'; | ||
import { | ||
CommonAndroidAppCredentialsFragment, | ||
GoogleServiceAccountKeyFragment, | ||
} from '../../../graphql/generated'; | ||
import Log from '../../../log'; | ||
import { promptAsync } from '../../../prompts'; | ||
import { CredentialsContext } from '../../context'; | ||
import { MissingCredentialsNonInteractiveError } from '../../errors'; | ||
import { AppLookupParams } from '../api/GraphqlClient'; | ||
|
||
export class SetUpGoogleServiceAccountKeyForFcmV1 { | ||
constructor(private app: AppLookupParams) {} | ||
|
||
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> { | ||
const isKeySetup = await this.isGoogleServiceAccountKeySetupAsync(ctx); | ||
if (isKeySetup) { | ||
Log.succeed('Google Service Account Key for FCM V1 already set up.'); | ||
return nullthrows( | ||
await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync( | ||
ctx.graphqlClient, | ||
this.app | ||
), | ||
'androidAppCredentials cannot be null if google service account key is already set up' | ||
); | ||
} | ||
if (ctx.nonInteractive) { | ||
throw new MissingCredentialsNonInteractiveError( | ||
'Google Service Account Keys cannot be set up in --non-interactive mode.' | ||
); | ||
} | ||
|
||
const keysForAccount = await ctx.android.getGoogleServiceAccountKeysForAccountAsync( | ||
ctx.graphqlClient, | ||
this.app.account | ||
); | ||
let googleServiceAccountKey = null; | ||
if (keysForAccount.length === 0) { | ||
googleServiceAccountKey = await new CreateGoogleServiceAccountKeyForFcmV1( | ||
this.app.account | ||
).runAsync(ctx); | ||
} else { | ||
googleServiceAccountKey = await this.createOrUseExistingKeyAsync(ctx); | ||
} | ||
return await new AssignGoogleServiceAccountKeyForFcmV1(this.app).runAsync( | ||
ctx, | ||
googleServiceAccountKey | ||
); | ||
} | ||
|
||
private async isGoogleServiceAccountKeySetupAsync(ctx: CredentialsContext): Promise<boolean> { | ||
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync( | ||
ctx.graphqlClient, | ||
this.app | ||
); | ||
return !!appCredentials?.googleServiceAccountKeyForFcmV1; | ||
} | ||
|
||
private async createOrUseExistingKeyAsync( | ||
ctx: CredentialsContext | ||
): Promise<GoogleServiceAccountKeyFragment> { | ||
const { action } = await promptAsync({ | ||
type: 'select', | ||
name: 'action', | ||
message: 'Select the Google Service Account Key to use for FCM V1:', | ||
choices: [ | ||
{ | ||
title: '[Choose an existing key]', | ||
value: 'CHOOSE_EXISTING', | ||
}, | ||
{ title: '[Upload a new service account key]', value: 'GENERATE' }, | ||
], | ||
}); | ||
|
||
if (action === 'GENERATE') { | ||
return await new CreateGoogleServiceAccountKeyForFcmV1(this.app.account).runAsync(ctx); | ||
} | ||
return ( | ||
(await new UseExistingGoogleServiceAccountKey(this.app.account).runAsync(ctx)) ?? | ||
(await this.createOrUseExistingKeyAsync(ctx)) | ||
); | ||
} | ||
} |
Oops, something went wrong.