Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,10 @@
"../forms/src/**/*.spec.ts",
"../offline/src/**/*.spec.ts",
"../auth-firebase/src/**/*.spec.ts",
"../auth-firebase/social/src/**/*.spec.ts"
"../auth-firebase/social/src/**/*.spec.ts",
"../auth-firebase/google/src/**/*.spec.ts",
"../auth-firebase/apple/src/**/*.spec.ts",
"../auth-firebase/facebook/src/**/*.spec.ts"
]
},
"configurations": {
Expand Down
49 changes: 21 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@
"@angular/cli": "^22.1.0",
"@angular/compiler-cli": "^22.1.0",
"@angular/service-worker": "^22.1.0",
"@capacitor-community/apple-sign-in": "github:rdlabo/apple-sign-in#cap8_build",
"@capacitor-community/facebook-login": "^8.0.0",
"@capacitor-community/in-app-review": "^8.0.0",
"@capacitor/cli": ">=7.0.0 <9.0.0",
Expand Down Expand Up @@ -108,7 +107,8 @@
"typescript": "~6.0.0",
"typescript-eslint": "^8.61.1",
"vitest": "^4.0.0",
"write-pkg": "^7.0.0"
"write-pkg": "^7.0.0",
"@capawesome/capacitor-apple-sign-in": "^0.1.4"
},
"peerDependencies": {
"@angular/cdk": ">=21.0.0 <23.0.0"
Expand Down
32 changes: 19 additions & 13 deletions projects/kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,27 @@ Firebase, social login, Live Update, Preferences, Status Bar, in-app review, and

The `/auth-firebase/google` entry point uses `@capawesome/capacitor-google-sign-in` 0.1.x and therefore requires Capacitor 8. Applications remaining on Capacitor 7 can continue to use the core Kit and other compatible entry points, but cannot use this Google entry point.

Install only the native plugin for the provider entry point you import. `/auth-firebase/social` is deprecated but retained for backwards compatibility; it reexports Apple and Facebook and therefore requires both plugins. Migrate imports to `/auth-firebase/apple` and `/auth-firebase/facebook` when convenient; removal is not part of this change. `/auth-firebase/internal` is a provider-neutral shared implementation used by those entry points; it is not a supported application API.

Social login callbacks pin request identity via the authenticated `user` on the success payload. Failures in `before`/`success` (and Google `exchange`) are reported to `error`, return `{ status: false }`, and always run `finally`. Failures inside `error`/`finally` themselves reject the call. Native Apple runs on iOS only; web uses the Firebase popup. Cancellation is classified only when the native adapter preserves a cancellation code; unknown native errors are `other` (do not infer cancellation from localized messages).

## Entry points

| Import | Responsibility |
| ------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `@rdlabo/ionic-angular-kit` | Storage, overlays, guards, HTTP, realtime, directives, keyboard, and utilities |
| `@rdlabo/ionic-angular-kit/offline` | **Experimental.** Scoped local replica, outbox, pull, replay, and request policies |
| `@rdlabo/ionic-angular-kit/theme` | Persisted light/dark theme and native status bar sync |
| `@rdlabo/ionic-angular-kit/forms` | Ionic error text and state classes for Angular Signal Forms |
| `@rdlabo/ionic-angular-kit/review` | Throttled native in-app review requests |
| `@rdlabo/ionic-angular-kit/printer` | DOM-to-PNG, Brother label, and PDF helpers |
| `@rdlabo/ionic-angular-kit/auth-firebase` | Firebase dependency wiring and authentication flows |
| `@rdlabo/ionic-angular-kit/auth-firebase/google` | Google popup/native sign-in, Firebase session linking, and logout |
| `@rdlabo/ionic-angular-kit/auth-firebase/social` | Apple and Facebook Firebase social-auth helpers |
| `@rdlabo/ionic-angular-kit/app-update` | Atomic Angular service-worker update transitions |
| `@rdlabo/ionic-angular-kit/live-update` | Capawesome Live Update readiness provider |
| Import | Responsibility |
| -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `@rdlabo/ionic-angular-kit` | Storage, overlays, guards, HTTP, realtime, directives, keyboard, and utilities |
| `@rdlabo/ionic-angular-kit/offline` | **Experimental.** Scoped local replica, outbox, pull, replay, and request policies |
| `@rdlabo/ionic-angular-kit/theme` | Persisted light/dark theme and native status bar sync |
| `@rdlabo/ionic-angular-kit/forms` | Ionic error text and state classes for Angular Signal Forms |
| `@rdlabo/ionic-angular-kit/review` | Throttled native in-app review requests |
| `@rdlabo/ionic-angular-kit/printer` | DOM-to-PNG, Brother label, and PDF helpers |
| `@rdlabo/ionic-angular-kit/auth-firebase` | Firebase dependency wiring and authentication flows |
| `@rdlabo/ionic-angular-kit/auth-firebase/apple` | Apple sign-in/link (own Apple plugin only) |
| `@rdlabo/ionic-angular-kit/auth-firebase/facebook` | Facebook login/link/logout (own Facebook plugin only) |
| `@rdlabo/ionic-angular-kit/auth-firebase/google` | Google popup/native sign-in, Firebase session linking, and logout |
| `@rdlabo/ionic-angular-kit/auth-firebase/social` | **Deprecated, retained for compatibility.** Apple/Facebook helpers |
| `@rdlabo/ionic-angular-kit/app-update` | Atomic Angular service-worker update transitions |
| `@rdlabo/ionic-angular-kit/live-update` | Capawesome Live Update readiness provider |

Secondary entry points isolate optional native and SDK dependencies from the core bundle.

Expand Down
6 changes: 6 additions & 0 deletions projects/kit/auth-firebase/apple/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
107 changes: 107 additions & 0 deletions projects/kit/auth-firebase/apple/src/kit-apple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Auth, User } from 'firebase/auth';
import {
EmailAuthProvider,
linkWithCredential,
linkWithPopup,
OAuthProvider,
reauthenticateWithPopup,
signInWithPopup,
} from 'firebase/auth';
import { Capacitor } from '@capacitor/core';
import { AppleSignIn, ErrorCode, SignInScope } from '@capawesome/capacitor-apple-sign-in';
import {
applyOAuthCredential,
assertCurrentUser,
classifyOAuthError,
requireUser,
runOAuthOperation,
} from '@rdlabo/ionic-angular-kit/auth-firebase/internal';
import type { KitOAuthMode, KitOAuthModeName, KitSocialHooks } from '@rdlabo/ionic-angular-kit/auth-firebase/internal';

/** Apple credentials. Native authorization codes and web access tokens are distinct values. */
export interface KitAppleResponse {
user: string | null;
email: string | null;
givenName: string | null;
familyName: string | null;
identityToken: string | null;
/** One-time authorization code from native Apple sign-in; null on web. */
authorizationCode: string | null;
/** Apple access token from the Firebase web popup; null on native.
* Optional for source compatibility with existing Apple response objects. Kit always supplies it.
*/
accessToken?: string | null;
}

/** Apple login options. The success hook receives the exact authenticated Firebase user. */
export type KitAppleLoginOptions = KitOAuthMode & KitSocialHooks<{ response: KitAppleResponse; mode: KitOAuthModeName; user: User }>;

const emptyAppleResponse = (): KitAppleResponse => ({
user: null,
email: null,
givenName: null,
familyName: null,
identityToken: null,
authorizationCode: null,
accessToken: null,
});

const classifyAppleError = (error: unknown) => {
const code = (error as { code?: string } | null)?.code;
return code === ErrorCode.SignInCanceled ? 'cancelled' : classifyOAuthError(error);
};

/** Apple sign-in/link on iOS and web. Native Android is not configured by this helper.
* Pins the user before any asynchronous work and reports SDK and app-hook failures to error.
*/
export const kitAppleLogin = async (auth: Auth, options: KitAppleLoginOptions): Promise<{ status: boolean }> => {
const expectedUser = auth.currentUser;
const execute = async (): Promise<void> => {
await options.before?.();
assertCurrentUser(auth, expectedUser);
let response: KitAppleResponse;
let user: User;
if (Capacitor.isNativePlatform()) {
if (Capacitor.getPlatform() !== 'ios') throw new Error('kit Apple login: native platform is not supported');
const native = await AppleSignIn.signIn({ scopes: [SignInScope.Email, SignInScope.FullName] });
response = {
user: native.user,
email: native.email,
givenName: native.givenName,
familyName: native.familyName,
identityToken: native.idToken,
authorizationCode: native.authorizationCode,
accessToken: null,
};
if (!response.identityToken) throw new Error('kit Apple login: identity token is missing');
const credential = new OAuthProvider('apple.com').credential({ idToken: response.identityToken });
user = await applyOAuthCredential(auth, credential, options, expectedUser);
} else {
const provider = new OAuthProvider('apple.com');
provider.addScope('email');
provider.addScope('name');
if (options.mode === 'credential') {
user = requireUser(expectedUser);
await reauthenticateWithPopup(user, provider);
assertCurrentUser(auth, user);
await linkWithCredential(user, EmailAuthProvider.credential(options.emailLogin.email, options.emailLogin.password));
response = emptyAppleResponse();
} else {
const result =
options.mode === 'new' ? await signInWithPopup(auth, provider) : await linkWithPopup(requireUser(expectedUser), provider);
user = result.user;
const credential = OAuthProvider.credentialFromResult(result);
response = {
...emptyAppleResponse(),
email: user.email,
identityToken: credential?.idToken ?? null,
accessToken: credential?.accessToken ?? null,
};
}
}
assertCurrentUser(auth, user);
await options.success?.({ response, mode: options.mode, user });
assertCurrentUser(auth, user);
};
return runOAuthOperation(auth, execute, (error) => options.error?.(classifyAppleError(error), error), options.finally);
};
2 changes: 2 additions & 0 deletions projects/kit/auth-firebase/apple/src/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './kit-apple';
export type { KitOAuthErrorCategory, KitOAuthMode, KitOAuthModeName } from '@rdlabo/ionic-angular-kit/auth-firebase/internal';
6 changes: 6 additions & 0 deletions projects/kit/auth-firebase/facebook/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}
85 changes: 85 additions & 0 deletions projects/kit/auth-firebase/facebook/src/kit-facebook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Auth, User } from 'firebase/auth';
import { FacebookAuthProvider, OAuthProvider } from 'firebase/auth';
import { Capacitor } from '@capacitor/core';
import { FacebookLogin } from '@capacitor-community/facebook-login';
import {
applyOAuthCredential,
assertCurrentUser,
classifyOAuthError,
runOAuthOperation,
} from '@rdlabo/ionic-angular-kit/auth-firebase/internal';
import type { KitOAuthMode, KitOAuthModeName, KitSocialHooks } from '@rdlabo/ionic-angular-kit/auth-firebase/internal';

/** Facebook login options. Permissions and app effects are owned by the consumer. */
export type KitFacebookLoginOptions = KitOAuthMode &
KitSocialHooks<{ accessToken: string; mode: KitOAuthModeName; user: User }> & {
permissions: string[];
};

const generateNonce = (): string =>
Array.from(crypto.getRandomValues(new Uint8Array(16)), (value) => value.toString(16).padStart(2, '0')).join('');

/** Await one animation frame where available (iOS WebView crash workaround; no-op off-browser). */
const nextFrame = (): Promise<void> =>
new Promise<void>((resolve) => {
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => resolve());
} else {
resolve();
}
});

/** Web plugin cancellation shape: the login promise rejects with a response whose token is null. */
const isFacebookCancellation = (error: unknown): boolean => {
if (typeof error !== 'object' || error === null || !('accessToken' in error)) {
return false;
}
const { accessToken } = error;
if (typeof accessToken !== 'object' || accessToken === null || !('token' in accessToken)) {
return false;
}
return accessToken.token === null;
};

/** Facebook login/link with a pinned Firebase user and classified failure hooks. */
export const kitFacebookLogin = async (auth: Auth, options: KitFacebookLoginOptions): Promise<{ status: boolean }> => {
const expectedUser = auth.currentUser;
const execute = async (): Promise<void> => {
await options.before?.();
assertCurrentUser(auth, expectedUser);
const nonce = generateNonce();
const event = await FacebookLogin.login({ permissions: options.permissions, nonce }).finally(nextFrame);
if (!event?.accessToken?.token) throw Object.assign(new Error('Facebook login cancelled'), { code: 'auth/user-cancelled' });
const accessToken = event.accessToken.token;
const credential =
Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios'
? new OAuthProvider('facebook.com').credential({ rawNonce: nonce, idToken: accessToken })
: FacebookAuthProvider.credential(accessToken);
const user = await applyOAuthCredential(auth, credential, options, expectedUser);
await options.success?.({ accessToken, mode: options.mode, user });
assertCurrentUser(auth, user);
};
return runOAuthOperation(
auth,
execute,
(error) => options.error?.(isFacebookCancellation(error) ? 'cancelled' : classifyOAuthError(error), error),
options.finally,
);
};

/**
* Log out of the Facebook SDK (best-effort; errors are ignored).
*
* @remarks
* Apps that offer Facebook login typically call this alongside the Firebase sign-out, so it lives
* here to keep the `@capacitor-community/facebook-login` import out of the app.
* Skips the call when the Facebook SDK has no active session — otherwise `FB.logout()` logs
* "called without an access token" on web and native rejects for email/password users.
*/
export const kitFacebookLogout = async (): Promise<void> => {
const session = await FacebookLogin.getCurrentAccessToken().catch(() => null);
if (!session?.accessToken?.token) {
return;
}
await FacebookLogin.logout().catch(() => undefined);
};
2 changes: 2 additions & 0 deletions projects/kit/auth-firebase/facebook/src/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './kit-facebook';
export type { KitOAuthErrorCategory, KitOAuthMode, KitOAuthModeName } from '@rdlabo/ionic-angular-kit/auth-firebase/internal';
Loading