feat(auth): Google/Apple login, named custom providers, identity link/unlink
This commit is contained in:
+126
-4
@@ -1,15 +1,21 @@
|
||||
/**
|
||||
* AuthService — player authentication.
|
||||
*
|
||||
* Handles login via device ID (the primary auth flow for game clients) or a
|
||||
* custom backend webhook, logout (token clearing), and auth state observation.
|
||||
* Handles login via device ID (the primary auth flow for game clients), Google,
|
||||
* Apple, or a custom backend webhook, identity linking/unlinking, logout
|
||||
* (token clearing), and auth state observation.
|
||||
*/
|
||||
|
||||
import type { RudderContext } from '../core/context.js';
|
||||
import type { RemoteConfigShape } from '../state/RemoteConfigState.js';
|
||||
import { getOrCreateDeviceId } from '../device/DeviceId.js';
|
||||
import { api } from '../generated/api.js';
|
||||
import type { LoginViaCustomResponse, LoginViaDeviceResponse } from '../generated/auth.js';
|
||||
import type {
|
||||
LoginViaAppleResponse,
|
||||
LoginViaCustomResponse,
|
||||
LoginViaDeviceResponse,
|
||||
LoginViaGoogleResponse,
|
||||
} from '../generated/auth.js';
|
||||
|
||||
export type AuthState = 'signed-in' | 'signed-out';
|
||||
|
||||
@@ -27,6 +33,8 @@ export interface LoginWithDeviceOptions {
|
||||
export interface LoginWithCustomOptions {
|
||||
/** Arbitrary payload forwarded to the developer's custom auth webhook. */
|
||||
customData: Record<string, unknown>;
|
||||
/** Name of the custom auth provider; omitted means the default provider. */
|
||||
provider?: string;
|
||||
/** Player's region code (default: "global"). */
|
||||
region?: string;
|
||||
/** Player's language code (default: "en"). */
|
||||
@@ -35,6 +43,35 @@ export interface LoginWithCustomOptions {
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface LoginWithGoogleOptions {
|
||||
/** Google ID token obtained from the Google sign-in flow. */
|
||||
idToken: string;
|
||||
/** Player's region code (default: "global"). */
|
||||
region?: string;
|
||||
/** Player's language code (default: "en"). */
|
||||
language?: string;
|
||||
/** Optional player nickname; omitted from the request when not set. */
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface LoginWithAppleOptions {
|
||||
/** Apple ID token (identity token) obtained from Sign in with Apple. */
|
||||
idToken: string;
|
||||
/** Player's region code (default: "global"). */
|
||||
region?: string;
|
||||
/** Player's language code (default: "en"). */
|
||||
language?: string;
|
||||
/** Optional player nickname; omitted from the request when not set. */
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export interface LinkWithCustomOptions {
|
||||
/** Arbitrary payload forwarded to the developer's custom auth webhook. */
|
||||
customData: Record<string, unknown>;
|
||||
/** Name of the custom auth provider; omitted means the default provider. */
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape> {
|
||||
private readonly listeners = new Set<AuthStateListener>();
|
||||
private state: AuthState;
|
||||
@@ -100,10 +137,11 @@ export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape>
|
||||
* saved to the client's TokenStore.
|
||||
*/
|
||||
async loginWithCustom(options: LoginWithCustomOptions): Promise<LoginViaCustomResponse> {
|
||||
const { customData, region = 'global', language = 'en', nickname } = options;
|
||||
const { customData, provider, region = 'global', language = 'en', nickname } = options;
|
||||
const response = await api.loginViaCustom(this.ctx, {
|
||||
key: this.ctx.options.projectKey,
|
||||
customData,
|
||||
provider,
|
||||
region,
|
||||
language,
|
||||
nickname,
|
||||
@@ -118,6 +156,90 @@ export class AuthService<TConfig extends RemoteConfigShape = RemoteConfigShape>
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with a Google ID token and returns access + refresh tokens.
|
||||
*
|
||||
* On success, tokens are saved to the client's TokenStore.
|
||||
*/
|
||||
async loginWithGoogle(options: LoginWithGoogleOptions): Promise<LoginViaGoogleResponse> {
|
||||
const { idToken, region = 'global', language = 'en', nickname } = options;
|
||||
const response = await api.loginViaGoogle(this.ctx, {
|
||||
key: this.ctx.options.projectKey,
|
||||
idToken,
|
||||
region,
|
||||
language,
|
||||
nickname,
|
||||
});
|
||||
|
||||
this.ctx.options.tokenStore.saveTokens(
|
||||
response.accessToken ?? '',
|
||||
response.refreshToken ?? '',
|
||||
);
|
||||
await this.startRuntime();
|
||||
this.setState('signed-in');
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with an Apple ID token and returns access + refresh tokens.
|
||||
*
|
||||
* On success, tokens are saved to the client's TokenStore.
|
||||
*/
|
||||
async loginWithApple(options: LoginWithAppleOptions): Promise<LoginViaAppleResponse> {
|
||||
const { idToken, region = 'global', language = 'en', nickname } = options;
|
||||
const response = await api.loginViaApple(this.ctx, {
|
||||
key: this.ctx.options.projectKey,
|
||||
idToken,
|
||||
region,
|
||||
language,
|
||||
nickname,
|
||||
});
|
||||
|
||||
this.ctx.options.tokenStore.saveTokens(
|
||||
response.accessToken ?? '',
|
||||
response.refreshToken ?? '',
|
||||
);
|
||||
await this.startRuntime();
|
||||
this.setState('signed-in');
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a Google identity to the currently signed-in player. Requires an
|
||||
* active session (the access token is sent automatically).
|
||||
*/
|
||||
async linkWithGoogle(idToken: string): Promise<void> {
|
||||
await api.linkViaGoogle(this.ctx, { idToken });
|
||||
}
|
||||
|
||||
/**
|
||||
* Links an Apple identity to the currently signed-in player. Requires an
|
||||
* active session (the access token is sent automatically).
|
||||
*/
|
||||
async linkWithApple(idToken: string): Promise<void> {
|
||||
await api.linkViaApple(this.ctx, { idToken });
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a custom identity to the currently signed-in player via the project's
|
||||
* custom authorization webhook. Requires an active session.
|
||||
*/
|
||||
async linkWithCustom(options: LinkWithCustomOptions): Promise<void> {
|
||||
await api.linkViaCustom(this.ctx, {
|
||||
customData: options.customData,
|
||||
provider: options.provider,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlinks an identity from the currently signed-in player. The provider is
|
||||
* the full provider string: `device`, `google`, `apple`, `custom`, or
|
||||
* `custom:<name>`.
|
||||
*/
|
||||
async unlinkIdentity(provider: string): Promise<void> {
|
||||
await api.unlinkIdentity(this.ctx, { provider });
|
||||
}
|
||||
|
||||
/** Clears all stored tokens (logout). */
|
||||
logout(): void {
|
||||
this.ctx.options.tokenStore.clear();
|
||||
|
||||
+37
-1
@@ -1,6 +1,6 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
|
||||
import type { LoginViaCustomRequest, LoginViaCustomResponse, LoginViaDeviceRequest, LoginViaDeviceResponse, RefreshAccessTokenRequest, RefreshAccessTokenResponse } from './auth.js';
|
||||
import type { LinkViaAppleRequest, LinkViaCustomRequest, LinkViaGoogleRequest, LoginViaAppleRequest, LoginViaAppleResponse, LoginViaCustomRequest, LoginViaCustomResponse, LoginViaDeviceRequest, LoginViaDeviceResponse, LoginViaGoogleRequest, LoginViaGoogleResponse, RefreshAccessTokenRequest, RefreshAccessTokenResponse, UnlinkIdentityRequest } from './auth.js';
|
||||
import type { AddBattlePassXpRequest, AddBattlePassXpResponse, ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse, GetBattlePassProgressRequest, GetBattlePassProgressResponse, PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse } from './battlepass.js';
|
||||
import type { ListCatalogItemsResponse } from './catalog.js';
|
||||
import type { GetInventoryResponse } from './inventory.js';
|
||||
@@ -118,6 +118,24 @@ export const api = {
|
||||
): Promise<HandleScenarioCallbackResponse> =>
|
||||
t.request<HandleScenarioCallbackResponse>('POST', '/sdk/v1/scenarios/callback', body),
|
||||
|
||||
linkViaApple: (
|
||||
t: Transport,
|
||||
body: LinkViaAppleRequest,
|
||||
): Promise<void> =>
|
||||
t.request<void>('POST', '/sdk/v1/authorization/apple/link', body),
|
||||
|
||||
linkViaCustom: (
|
||||
t: Transport,
|
||||
body: LinkViaCustomRequest,
|
||||
): Promise<void> =>
|
||||
t.request<void>('POST', '/sdk/v1/authorization/custom/link', body),
|
||||
|
||||
linkViaGoogle: (
|
||||
t: Transport,
|
||||
body: LinkViaGoogleRequest,
|
||||
): Promise<void> =>
|
||||
t.request<void>('POST', '/sdk/v1/authorization/google/link', body),
|
||||
|
||||
listCatalogItems: (
|
||||
t: Transport,
|
||||
): Promise<ListCatalogItemsResponse> =>
|
||||
@@ -143,6 +161,12 @@ export const api = {
|
||||
): Promise<ListStoresResponse> =>
|
||||
t.request<ListStoresResponse>('GET', '/sdk/v1/stores'),
|
||||
|
||||
loginViaApple: (
|
||||
t: Transport,
|
||||
body: LoginViaAppleRequest,
|
||||
): Promise<LoginViaAppleResponse> =>
|
||||
t.request<LoginViaAppleResponse>('POST', '/sdk/v1/authorization/apple', body),
|
||||
|
||||
loginViaCustom: (
|
||||
t: Transport,
|
||||
body: LoginViaCustomRequest,
|
||||
@@ -155,6 +179,12 @@ export const api = {
|
||||
): Promise<LoginViaDeviceResponse> =>
|
||||
t.request<LoginViaDeviceResponse>('POST', '/sdk/v1/authorization/device', body),
|
||||
|
||||
loginViaGoogle: (
|
||||
t: Transport,
|
||||
body: LoginViaGoogleRequest,
|
||||
): Promise<LoginViaGoogleResponse> =>
|
||||
t.request<LoginViaGoogleResponse>('POST', '/sdk/v1/authorization/google', body),
|
||||
|
||||
purchaseBattlePassPremium: (
|
||||
t: Transport,
|
||||
body: PurchaseBattlePassPremiumRequest,
|
||||
@@ -193,6 +223,12 @@ export const api = {
|
||||
): Promise<TriggerScenarioResponse> =>
|
||||
t.request<TriggerScenarioResponse>('POST', '/sdk/v1/scenarios/trigger', body),
|
||||
|
||||
unlinkIdentity: (
|
||||
t: Transport,
|
||||
body: UnlinkIdentityRequest,
|
||||
): Promise<void> =>
|
||||
t.request<void>('POST', '/sdk/v1/authorization/unlink', body),
|
||||
|
||||
updateProjectStorage: (
|
||||
t: Transport,
|
||||
body: UpdateProjectStorageRequest,
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
// Code generated by apigen. DO NOT EDIT.
|
||||
|
||||
export interface LinkViaAppleRequest {
|
||||
"idToken": string;
|
||||
}
|
||||
|
||||
export interface LinkViaCustomRequest {
|
||||
"customData": { [key: string]: unknown };
|
||||
"provider"?: string;
|
||||
}
|
||||
|
||||
export interface LinkViaGoogleRequest {
|
||||
"idToken": string;
|
||||
}
|
||||
|
||||
export interface LoginViaAppleRequest {
|
||||
"idToken": string;
|
||||
"key": string;
|
||||
"language": string;
|
||||
"nickname"?: string;
|
||||
"region": string;
|
||||
}
|
||||
|
||||
export interface LoginViaAppleResponse {
|
||||
"accessToken"?: string;
|
||||
"refreshToken"?: string;
|
||||
}
|
||||
|
||||
export interface LoginViaCustomRequest {
|
||||
"customData"?: { [key: string]: unknown };
|
||||
"key"?: string;
|
||||
"language"?: string;
|
||||
"nickname"?: string;
|
||||
"provider"?: string;
|
||||
"region"?: string;
|
||||
}
|
||||
|
||||
@@ -26,6 +53,19 @@ export interface LoginViaDeviceResponse {
|
||||
"refreshToken"?: string;
|
||||
}
|
||||
|
||||
export interface LoginViaGoogleRequest {
|
||||
"idToken": string;
|
||||
"key": string;
|
||||
"language": string;
|
||||
"nickname"?: string;
|
||||
"region": string;
|
||||
}
|
||||
|
||||
export interface LoginViaGoogleResponse {
|
||||
"accessToken"?: string;
|
||||
"refreshToken"?: string;
|
||||
}
|
||||
|
||||
export interface RefreshAccessTokenRequest {
|
||||
"refreshToken"?: string;
|
||||
}
|
||||
@@ -35,3 +75,7 @@ export interface RefreshAccessTokenResponse {
|
||||
"refreshToken"?: string;
|
||||
}
|
||||
|
||||
export interface UnlinkIdentityRequest {
|
||||
"provider": string;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,17 @@ export interface Player {
|
||||
}
|
||||
|
||||
export interface PlayerProfile {
|
||||
"identities"?: PlayerProfileIdentity[];
|
||||
"player"?: Player;
|
||||
"wallets"?: Wallet[];
|
||||
}
|
||||
|
||||
export interface PlayerProfileIdentity {
|
||||
"createdAt": string;
|
||||
"provider": string;
|
||||
"subject": string;
|
||||
}
|
||||
|
||||
export interface Wallet {
|
||||
"balance"?: number;
|
||||
"currency"?: string;
|
||||
|
||||
+4
-1
@@ -22,8 +22,11 @@ export type {
|
||||
AuthService,
|
||||
AuthState,
|
||||
AuthStateListener,
|
||||
LinkWithCustomOptions,
|
||||
LoginWithAppleOptions,
|
||||
LoginWithCustomOptions,
|
||||
LoginWithDeviceOptions,
|
||||
LoginWithGoogleOptions,
|
||||
} from './auth/AuthService.js';
|
||||
export type { LeaderboardsService, LeaderboardHandle } from './leaderboards/LeaderboardsService.js';
|
||||
export type { BattlePassService } from './battlepass/BattlePassService.js';
|
||||
@@ -71,7 +74,7 @@ export type {
|
||||
|
||||
// Generated wire types — the shapes returned by the observable state and
|
||||
// handles, so consumers can name them instead of re-declaring mirrors.
|
||||
export type { Player, PlayerProfile, Wallet } from './generated/player.js';
|
||||
export type { Player, PlayerProfile, PlayerProfileIdentity, Wallet } from './generated/player.js';
|
||||
export type { CatalogItem } from './generated/catalog.js';
|
||||
export type { PlayerInventoryItem } from './generated/inventory.js';
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user