@ng-native/firebase-messaging 0.0.0-stage → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ashley Hunter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @ng-native/firebase-messaging
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The app's Firebase Installation ID (FID), registered with Firebase Cloud Messaging, for a backend
4
+ that sends push notifications through FCM's HTTP v1 API with the `fid` target.
5
+
6
+ Alpha: APIs may change before 1.0.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npx expo install @ng-native/firebase-messaging expo-notifications
12
+ ```
13
+
14
+ Add `"@ng-native/firebase-messaging"` to the plugins in `app.json`, with Expo's
15
+ `ios.googleServicesFile` and `android.googleServicesFile`, then rebuild the app: it needs a
16
+ development build, not Expo Go.
17
+
18
+ ## Example
19
+
20
+ ```ts
21
+ import { Service, effect, inject } from '@angular/core';
22
+ import { FirebaseMessaging } from '@ng-native/firebase-messaging';
23
+
24
+ @Service()
25
+ export class Push {
26
+ private readonly messaging = inject(FirebaseMessaging);
27
+
28
+ constructor() {
29
+ effect(() => {
30
+ const fid = this.messaging.installationId();
31
+ if (fid) void sendToBackend(fid);
32
+ });
33
+ }
34
+
35
+ enable(): Promise<string | null> {
36
+ return this.messaging.register();
37
+ }
38
+ }
39
+ ```
40
+
41
+ The documentation is at
42
+ [ng-native.com/packages/firebase-messaging](https://ng-native.com/packages/firebase-messaging).
@@ -0,0 +1,20 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'dev.ngnative.firebasemessaging'
7
+ version = '1.0.0'
8
+
9
+ android {
10
+ namespace "dev.ngnative.firebasemessaging"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName '1.0.0'
14
+ }
15
+ }
16
+
17
+ dependencies {
18
+ // The version the reporter's FID registration was built on; FirebaseInstallations comes with it.
19
+ implementation 'com.google.firebase:firebase-messaging:25.1.2'
20
+ }
@@ -0,0 +1,34 @@
1
+ package dev.ngnative.firebasemessaging
2
+
3
+ import com.google.android.gms.tasks.Task
4
+ import com.google.firebase.installations.FirebaseInstallations
5
+ import com.google.firebase.messaging.FirebaseMessaging
6
+ import expo.modules.kotlin.Promise
7
+ import expo.modules.kotlin.modules.Module
8
+ import expo.modules.kotlin.modules.ModuleDefinition
9
+
10
+ /**
11
+ * The app's Firebase Installation ID (FID), and its registration with FCM, so a backend can send
12
+ * to it through FCM's HTTP v1 API with the message's `fid` target. Android has no APNs token, so
13
+ * `register` ignores its argument.
14
+ */
15
+ class FirebaseMessagingModule : Module() {
16
+ override fun definition() = ModuleDefinition {
17
+ Name("NgNativeFirebaseMessaging")
18
+
19
+ AsyncFunction("getInstallationId") { promise: Promise ->
20
+ settle(FirebaseInstallations.getInstance().id, promise)
21
+ }
22
+
23
+ AsyncFunction("register") { _: String?, promise: Promise ->
24
+ settle(FirebaseMessaging.getInstance().register(), promise)
25
+ }
26
+ }
27
+
28
+ private fun <T> settle(task: Task<T>, promise: Promise) {
29
+ task.addOnCompleteListener {
30
+ if (it.isSuccessful) promise.resolve(it.result)
31
+ else promise.reject("ERR_FIREBASE_MESSAGING", it.exception?.message, it.exception)
32
+ }
33
+ }
34
+ }
package/app.plugin.cjs ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `"plugins": ["@ng-native/firebase-messaging"]` in app.json: turns on FCM registration by
3
+ * installation ID, which both platforms leave off (registering then fails with "API disabled"),
4
+ * and turns off Firebase's app delegate proxy on iOS, which would swizzle the push methods
5
+ * `expo-notifications` owns. The APNs token reaches FCM through `register()` instead.
6
+ *
7
+ * In the Podfile it gives `GoogleUtilities` modules, without which CocoaPods refuses to link
8
+ * Firebase's Swift pods as the static libraries an Expo app builds.
9
+ *
10
+ * Firebase's config files are Expo's own `ios.googleServicesFile` and `android.googleServicesFile`.
11
+ */
12
+ const MANIFEST_FLAG = 'firebase_messaging_installation_id_enabled';
13
+
14
+ function infoPlist(plist) {
15
+ plist.FirebaseMessagingInstallationIdEnabled = true;
16
+ plist.FirebaseAppDelegateProxyEnabled = false;
17
+ return plist;
18
+ }
19
+
20
+ const MODULAR = "pod 'GoogleUtilities', :modular_headers => true";
21
+
22
+ function podfile(contents) {
23
+ if (/^[ \t]*pod ['"]GoogleUtilities['"]/m.test(contents)) return contents;
24
+ const expo = /^([ \t]*)use_expo_modules!.*$/m.exec(contents);
25
+ if (!expo) throw new Error('[angular-native] The Podfile has no use_expo_modules! to add to.');
26
+ const end = expo.index + expo[0].length;
27
+ return `${contents.slice(0, end)}\n${expo[1]}${MODULAR}${contents.slice(end)}`;
28
+ }
29
+
30
+ function androidManifest(manifest) {
31
+ const application = manifest.manifest.application?.[0];
32
+ if (!application) throw new Error('[angular-native] AndroidManifest.xml has no <application>.');
33
+ const items = (application['meta-data'] ??= []);
34
+ const found = items.find((item) => item.$['android:name'] === MANIFEST_FLAG);
35
+ if (found) found.$['android:value'] = 'true';
36
+ else items.push({ $: { 'android:name': MANIFEST_FLAG, 'android:value': 'true' } });
37
+ return manifest;
38
+ }
39
+
40
+ module.exports = function withFirebaseMessaging(config) {
41
+ const projectRoot = config._internal?.projectRoot ?? process.cwd();
42
+ const { withInfoPlist, withPodfile, withAndroidManifest } = require(
43
+ require.resolve('expo/config-plugins', { paths: [projectRoot] }),
44
+ );
45
+ const withPlist = withInfoPlist(config, (ios) => {
46
+ ios.modResults = infoPlist(ios.modResults);
47
+ return ios;
48
+ });
49
+ const withPods = withPodfile(withPlist, (pods) => {
50
+ pods.modResults.contents = podfile(pods.modResults.contents);
51
+ return pods;
52
+ });
53
+ return withAndroidManifest(withPods, (android) => {
54
+ android.modResults = androidManifest(android.modResults);
55
+ return android;
56
+ });
57
+ };
58
+
59
+ module.exports.infoPlist = infoPlist;
60
+ module.exports.podfile = podfile;
61
+ module.exports.androidManifest = androidManifest;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `FirebaseMessaging`: the app's Firebase Installation ID (FID), registered with FCM, for a backend
3
+ * that sends through FCM's HTTP v1 API to the message's `fid` target.
4
+ *
5
+ * ```ts
6
+ * import { FirebaseMessaging } from '@ng-native/firebase-messaging';
7
+ *
8
+ * private readonly messaging = inject(FirebaseMessaging);
9
+ *
10
+ * constructor() {
11
+ * effect(() => {
12
+ * const fid = this.messaging.installationId();
13
+ * if (fid) void this.api.savePushTarget(fid);
14
+ * });
15
+ * }
16
+ *
17
+ * enablePush(): Promise<string | null> {
18
+ * return this.messaging.register();
19
+ * }
20
+ * ```
21
+ *
22
+ * FCM rejects a send to an FID with `UNREGISTERED` until `register()` has run. On iOS FCM delivers
23
+ * over APNs, so registering takes the APNs token `Notifications` reports; on Android it needs no
24
+ * token. Either way it asks for the notification permission first. When the platform rolls the
25
+ * push token, the installation is registered again, and `installationId` is set again. Firebase
26
+ * can also replace the FID by itself, so each time the app comes back to the front the FID is read
27
+ * again, and `installationId` follows it.
28
+ *
29
+ * Without the native module (Expo Go, the web host, a test that provides nothing) every call
30
+ * answers null.
31
+ */
32
+ import { InjectionToken, type Signal } from '@angular/core';
33
+ import * as i0 from "@angular/core";
34
+ /** The native module's functions. A test provides a fake. */
35
+ export interface NativeFirebaseMessaging {
36
+ getInstallationId(): Promise<string>;
37
+ /** Registers the installation with FCM. iOS needs the APNs token as hex; Android ignores it. */
38
+ register(apnsToken: string | null): Promise<void>;
39
+ }
40
+ export declare class FirebaseMessaging {
41
+ /** Null in a build without the package's native half. */
42
+ static readonly SOURCE: InjectionToken<NativeFirebaseMessaging | null>;
43
+ private readonly native;
44
+ private readonly notifications;
45
+ private readonly errors;
46
+ private readonly os;
47
+ private readonly id;
48
+ private registered;
49
+ /** Counts FID reads, so one that settles after a newer one does not overwrite it. */
50
+ private reads;
51
+ /** The FID, each time a registration answers; null until one has. For the app's backend. */
52
+ readonly installationId: Signal<string | null>;
53
+ constructor();
54
+ /**
55
+ * Registers this installation with FCM and answers its FID. Null without the notification
56
+ * permission or the native module.
57
+ */
58
+ register(): Promise<string | null>;
59
+ /** The FID, without registering it. Null without the native module. */
60
+ getInstallationId(): Promise<string | null>;
61
+ /** Firebase replaces an FID it can no longer use without saying so to JavaScript. */
62
+ private readAgain;
63
+ private registerWith;
64
+ private read;
65
+ static ɵfac: i0.ɵɵFactoryDeclaration<FirebaseMessaging, never>;
66
+ static ɵprov: i0.ɵɵInjectableDeclaration<any>;
67
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `FirebaseMessaging`: the app's Firebase Installation ID (FID), registered with FCM, for a backend
3
+ * that sends through FCM's HTTP v1 API to the message's `fid` target.
4
+ *
5
+ * ```ts
6
+ * import { FirebaseMessaging } from '@ng-native/firebase-messaging';
7
+ *
8
+ * private readonly messaging = inject(FirebaseMessaging);
9
+ *
10
+ * constructor() {
11
+ * effect(() => {
12
+ * const fid = this.messaging.installationId();
13
+ * if (fid) void this.api.savePushTarget(fid);
14
+ * });
15
+ * }
16
+ *
17
+ * enablePush(): Promise<string | null> {
18
+ * return this.messaging.register();
19
+ * }
20
+ * ```
21
+ *
22
+ * FCM rejects a send to an FID with `UNREGISTERED` until `register()` has run. On iOS FCM delivers
23
+ * over APNs, so registering takes the APNs token `Notifications` reports; on Android it needs no
24
+ * token. Either way it asks for the notification permission first. When the platform rolls the
25
+ * push token, the installation is registered again, and `installationId` is set again. Firebase
26
+ * can also replace the FID by itself, so each time the app comes back to the front the FID is read
27
+ * again, and `installationId` follows it.
28
+ *
29
+ * Without the native module (Expo Go, the web host, a test that provides nothing) every call
30
+ * answers null.
31
+ */
32
+ import { DestroyRef, ErrorHandler, InjectionToken, Service, effect, inject, signal, } from '@angular/core';
33
+ import { OS } from '@ng-native/device';
34
+ import { Permission } from '@ng-native/expo';
35
+ import { Notifications } from '@ng-native/expo/notifications';
36
+ import * as i0 from "@angular/core";
37
+ export class FirebaseMessaging {
38
+ /** Null in a build without the package's native half. */
39
+ static SOURCE = new InjectionToken('angular-native.firebaseMessagingSource', {
40
+ factory: () => globalThis.expo?.modules?.NgNativeFirebaseMessaging ?? null,
41
+ });
42
+ native = inject(FirebaseMessaging.SOURCE);
43
+ notifications = inject(Notifications);
44
+ errors = inject(ErrorHandler);
45
+ os = inject(OS);
46
+ id = signal(null, /* @ts-ignore */
47
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
48
+ registered = false;
49
+ /** Counts FID reads, so one that settles after a newer one does not overwrite it. */
50
+ reads = 0;
51
+ /** The FID, each time a registration answers; null until one has. For the app's backend. */
52
+ installationId = this.id.asReadonly();
53
+ constructor() {
54
+ // A rolled APNs token is one FCM does not have, and pushes stop arriving until it does. A
55
+ // registration that fails goes to the ErrorHandler, since nothing awaits it.
56
+ effect(() => {
57
+ const token = this.notifications.devicePushToken();
58
+ if (!token || !this.registered)
59
+ return;
60
+ this.registerWith(this.os === 'ios' ? token.data : null).catch((failure) => this.errors.handleError(failure));
61
+ });
62
+ const stop = inject(Permission.FOREGROUND)?.(() => this.readAgain());
63
+ if (stop)
64
+ inject(DestroyRef).onDestroy(stop);
65
+ }
66
+ /**
67
+ * Registers this installation with FCM and answers its FID. Null without the notification
68
+ * permission or the native module.
69
+ */
70
+ async register() {
71
+ if (!this.native)
72
+ return null;
73
+ // Android needs no token, and asking for one would fetch FCM's deprecated registration token.
74
+ if (this.os !== 'ios') {
75
+ return (await this.notifications.permission.ensure()) ? this.registerWith(null) : null;
76
+ }
77
+ const token = await this.notifications.getDevicePushToken();
78
+ return token ? this.registerWith(token.data) : null;
79
+ }
80
+ /** The FID, without registering it. Null without the native module. */
81
+ async getInstallationId() {
82
+ return (await this.native?.getInstallationId()) ?? null;
83
+ }
84
+ /** Firebase replaces an FID it can no longer use without saying so to JavaScript. */
85
+ readAgain() {
86
+ const native = this.native;
87
+ if (!native || !this.registered)
88
+ return;
89
+ this.read(native).catch((failure) => this.errors.handleError(failure));
90
+ }
91
+ async registerWith(apnsToken) {
92
+ const native = this.native;
93
+ if (!native)
94
+ return null;
95
+ await native.register(apnsToken);
96
+ this.registered = true;
97
+ return this.read(native);
98
+ }
99
+ async read(native) {
100
+ const read = ++this.reads;
101
+ const id = await native.getInstallationId();
102
+ if (read === this.reads)
103
+ this.id.set(id);
104
+ return id;
105
+ }
106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: FirebaseMessaging, deps: [], target: i0.ɵɵFactoryTarget.Service });
107
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: FirebaseMessaging });
108
+ }
109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: FirebaseMessaging, decorators: [{
110
+ type: Service
111
+ }], ctorParameters: () => [] });
112
+ //# sourceMappingURL=firebase-messaging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firebase-messaging.js","sourceRoot":"","sources":["../src/firebase-messaging.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `FirebaseMessaging`: the app's Firebase Installation ID (FID), registered with FCM, for a backend\n * that sends through FCM's HTTP v1 API to the message's `fid` target.\n *\n * ```ts\n * import { FirebaseMessaging } from '@ng-native/firebase-messaging';\n *\n * private readonly messaging = inject(FirebaseMessaging);\n *\n * constructor() {\n * effect(() => {\n * const fid = this.messaging.installationId();\n * if (fid) void this.api.savePushTarget(fid);\n * });\n * }\n *\n * enablePush(): Promise<string | null> {\n * return this.messaging.register();\n * }\n * ```\n *\n * FCM rejects a send to an FID with `UNREGISTERED` until `register()` has run. On iOS FCM delivers\n * over APNs, so registering takes the APNs token `Notifications` reports; on Android it needs no\n * token. Either way it asks for the notification permission first. When the platform rolls the\n * push token, the installation is registered again, and `installationId` is set again. Firebase\n * can also replace the FID by itself, so each time the app comes back to the front the FID is read\n * again, and `installationId` follows it.\n *\n * Without the native module (Expo Go, the web host, a test that provides nothing) every call\n * answers null.\n */\nimport {\n DestroyRef,\n ErrorHandler,\n InjectionToken,\n Service,\n effect,\n inject,\n signal,\n type Signal,\n} from '@angular/core';\nimport { OS } from '@ng-native/device';\nimport { Permission } from '@ng-native/expo';\nimport { Notifications } from '@ng-native/expo/notifications';\n\n/** The native module's functions. A test provides a fake. */\nexport interface NativeFirebaseMessaging {\n getInstallationId(): Promise<string>;\n /** Registers the installation with FCM. iOS needs the APNs token as hex; Android ignores it. */\n register(apnsToken: string | null): Promise<void>;\n}\n\n@Service()\nexport class FirebaseMessaging {\n /** Null in a build without the package's native half. */\n static readonly SOURCE = new InjectionToken<NativeFirebaseMessaging | null>(\n 'angular-native.firebaseMessagingSource',\n {\n factory: () =>\n (\n globalThis as {\n expo?: { modules?: { NgNativeFirebaseMessaging?: NativeFirebaseMessaging } };\n }\n ).expo?.modules?.NgNativeFirebaseMessaging ?? null,\n },\n );\n\n private readonly native = inject(FirebaseMessaging.SOURCE);\n private readonly notifications = inject(Notifications);\n private readonly errors = inject(ErrorHandler);\n private readonly os = inject(OS);\n private readonly id = signal<string | null>(null);\n private registered = false;\n /** Counts FID reads, so one that settles after a newer one does not overwrite it. */\n private reads = 0;\n\n /** The FID, each time a registration answers; null until one has. For the app's backend. */\n readonly installationId: Signal<string | null> = this.id.asReadonly();\n\n constructor() {\n // A rolled APNs token is one FCM does not have, and pushes stop arriving until it does. A\n // registration that fails goes to the ErrorHandler, since nothing awaits it.\n effect(() => {\n const token = this.notifications.devicePushToken();\n if (!token || !this.registered) return;\n this.registerWith(this.os === 'ios' ? token.data : null).catch((failure: unknown) =>\n this.errors.handleError(failure),\n );\n });\n const stop = inject(Permission.FOREGROUND)?.(() => this.readAgain());\n if (stop) inject(DestroyRef).onDestroy(stop);\n }\n\n /**\n * Registers this installation with FCM and answers its FID. Null without the notification\n * permission or the native module.\n */\n async register(): Promise<string | null> {\n if (!this.native) return null;\n // Android needs no token, and asking for one would fetch FCM's deprecated registration token.\n if (this.os !== 'ios') {\n return (await this.notifications.permission.ensure()) ? this.registerWith(null) : null;\n }\n const token = await this.notifications.getDevicePushToken();\n return token ? this.registerWith(token.data) : null;\n }\n\n /** The FID, without registering it. Null without the native module. */\n async getInstallationId(): Promise<string | null> {\n return (await this.native?.getInstallationId()) ?? null;\n }\n\n /** Firebase replaces an FID it can no longer use without saying so to JavaScript. */\n private readAgain(): void {\n const native = this.native;\n if (!native || !this.registered) return;\n this.read(native).catch((failure: unknown) => this.errors.handleError(failure));\n }\n\n private async registerWith(apnsToken: string | null): Promise<string | null> {\n const native = this.native;\n if (!native) return null;\n await native.register(apnsToken);\n this.registered = true;\n return this.read(native);\n }\n\n private async read(native: NativeFirebaseMessaging): Promise<string> {\n const read = ++this.reads;\n const id = await native.getInstallationId();\n if (read === this.reads) this.id.set(id);\n return id;\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export { FirebaseMessaging, type NativeFirebaseMessaging } from './firebase-messaging.ts';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { FirebaseMessaging } from "./firebase-messaging.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAgC,MAAM,yBAAyB,CAAC","sourcesContent":["export { FirebaseMessaging, type NativeFirebaseMessaging } from './firebase-messaging.ts';\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["FirebaseMessagingModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["dev.ngnative.firebasemessaging.FirebaseMessagingModule"]
8
+ }
9
+ }
@@ -0,0 +1,60 @@
1
+ import ExpoModulesCore
2
+ import FirebaseCore
3
+ import FirebaseInstallations
4
+ import FirebaseMessaging
5
+
6
+ /**
7
+ * The app's Firebase Installation ID (FID), and its registration with FCM, so a backend can send
8
+ * to it through FCM's HTTP v1 API with the message's `fid` target.
9
+ *
10
+ * FCM delivers over APNs on iOS, so `register` takes the APNs token `expo-notifications` reported,
11
+ * as the lowercase hex it hands JavaScript. Firebase's app delegate proxy is off (the config
12
+ * plugin turns it off, since `expo-notifications` owns those methods), so this is the only way the
13
+ * token reaches FCM.
14
+ */
15
+ public class FirebaseMessagingModule: Module {
16
+ public func definition() -> ModuleDefinition {
17
+ Name("NgNativeFirebaseMessaging")
18
+
19
+ AsyncFunction("getInstallationId") { () async throws -> String in
20
+ try await Self.configure()
21
+ return try await Installations.installations().installationID()
22
+ }
23
+
24
+ AsyncFunction("register") { (apnsToken: String?) async throws in
25
+ try await Self.configure()
26
+ guard let apnsToken, let token = Self.data(hex: apnsToken) else {
27
+ throw Exception(
28
+ name: "ERR_APNS_TOKEN",
29
+ description: "FCM needs the APNs token on iOS, and none was given.")
30
+ }
31
+ await MainActor.run { Messaging.messaging().apnsToken = token }
32
+ try await Messaging.messaging().register()
33
+ }
34
+ }
35
+
36
+ /** Configures Firebase on first use unless the app has; without the plist, configure() aborts. */
37
+ @MainActor
38
+ private static func configure() throws {
39
+ if FirebaseApp.app() != nil { return }
40
+ guard Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist") != nil else {
41
+ throw Exception(
42
+ name: "ERR_NO_FIREBASE_CONFIG",
43
+ description: "GoogleService-Info.plist is not in the app. "
44
+ + "Set ios.googleServicesFile in app.json and rebuild.")
45
+ }
46
+ FirebaseApp.configure()
47
+ }
48
+
49
+ private static func data(hex: String) -> Data? {
50
+ let bytes = Array(hex.utf8)
51
+ guard bytes.count % 2 == 0 else { return nil }
52
+ var data = Data(capacity: bytes.count / 2)
53
+ for index in stride(from: 0, to: bytes.count, by: 2) {
54
+ let pair = String(decoding: bytes[index..<index + 2], as: UTF8.self)
55
+ guard let byte = UInt8(pair, radix: 16) else { return nil }
56
+ data.append(byte)
57
+ }
58
+ return data
59
+ }
60
+ }
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'NgNativeFirebaseMessaging'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = 'Angular Native'
12
+ s.homepage = 'https://github.com/ng-native/ng-native'
13
+ s.platforms = { :ios => '16.4' }
14
+ s.swift_version = '5.9'
15
+ s.source = { git: 'https://github.com/ng-native/ng-native.git' }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+ s.dependency 'FirebaseCore'
20
+ s.dependency 'FirebaseInstallations'
21
+ # 12.16 added registration by installation ID; 12.18 fixed it for an app with a legacy FCM token
22
+ # cached.
23
+ s.dependency 'FirebaseMessaging', '>= 12.18.0'
24
+
25
+ s.source_files = '**/*.swift'
26
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
27
+ end
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@ng-native/firebase-messaging",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.19.0",
4
+ "description": "Firebase Cloud Messaging by installation ID for Angular Native: registers the app's Firebase Installation ID with FCM, for a backend that sends through FCM's HTTP v1 API.",
5
+ "keywords": [
6
+ "angular",
7
+ "react-native",
8
+ "angular-native",
9
+ "expo",
10
+ "firebase",
11
+ "fcm",
12
+ "push-notifications",
13
+ "ios",
14
+ "android"
15
+ ],
16
+ "type": "module",
17
+ "main": "dist/index.js",
18
+ "peerDependencies": {
19
+ "@angular/core": "^22.0.0",
20
+ "@ng-native/device": "0.19.0",
21
+ "@ng-native/expo": "0.19.0",
22
+ "expo": "^57.0.0 || ^58.0.0",
23
+ "expo-notifications": "^57.0.0 || ^58.0.0"
24
+ },
25
+ "nx": {
26
+ "tags": [
27
+ "scope:firebase-messaging",
28
+ "layer:expo-addon"
29
+ ]
30
+ },
31
+ "license": "MIT",
32
+ "homepage": "https://ng-native.com/packages/firebase-messaging",
33
+ "bugs": {
34
+ "url": "https://github.com/ng-native/ng-native/issues"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/ng-native/ng-native.git",
39
+ "directory": "packages/firebase-messaging"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "app.plugin.cjs",
44
+ "expo-module.config.json",
45
+ "ios",
46
+ "android/build.gradle",
47
+ "android/src"
48
+ ],
49
+ "devDependencies": {
50
+ "@angular/core": "22.2.0",
51
+ "@babel/core": "^7.29.7",
52
+ "@ng-native/device": "0.19.0",
53
+ "@ng-native/expo": "0.19.0",
54
+ "typescript": "~6.0.3"
55
+ },
56
+ "exports": {
57
+ ".": {
58
+ "types": "./dist/index.d.ts",
59
+ "default": "./dist/index.js"
60
+ },
61
+ "./app.plugin.cjs": "./app.plugin.cjs",
62
+ "./package.json": "./package.json"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "scripts": {
68
+ "build": "ngc -p tsconfig.build.json",
69
+ "typecheck": "tsc -p tsconfig.json --noEmit"
70
+ },
71
+ "types": "dist/index.d.ts"
6
72
  }