Skip to content
Draft
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
117 changes: 117 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# cordova-outsystems-firebase-cloud-messaging Architecture

> **Repository:** cordova-outsystems-firebase-cloud-messaging
> **Runtime Environment:** Mobile Applications (iOS/Android native code embedded in hybrid apps)
> **Last Updated:** 2026-03-16

## Overview

This is a Cordova/Capacitor plugin that provides Firebase Cloud Messaging (FCM) capabilities to hybrid mobile applications. The plugin runs as native code (Swift on iOS, Kotlin on Android) within the host mobile application, bridging JavaScript calls from the WebView to native Firebase SDK functionality.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Cordova Plugin, that is mean for use in OutSystems mobile apps, with either Cordova or Capacitor framework, believe that should be clarified.


## Architecture Diagram

```mermaid
graph TB
%% This repository
ThisRepo["cordova-outsystems-firebase-cloud-messaging<br/>Runs on: Mobile Device (iOS/Android)<br/>Acts as: Cordova Plugin Bridge"]

%% External services
FCM[Firebase Cloud Messaging<br/>EXTERNAL]
APNs[Apple Push Notification Service<br/>EXTERNAL]
LocalDB[(Local SQLite/Room Database<br/>EXTERNAL)]
Azure[OutSystems Azure Artifact Repository<br/>EXTERNAL]
CocoaPods[CocoaPods CDN<br/>EXTERNAL]

%% Communication flows
ThisRepo -->|HTTP/gRPC<br/>Synchronous| FCM
ThisRepo -->|Native API<br/>Synchronous| APNs
ThisRepo -->|SQL queries<br/>Synchronous| LocalDB

%% Build-time dependencies
ThisRepo -.->|Fetch dependencies<br/>Build-time only| Azure
ThisRepo -.->|Fetch FirebaseMessaging pod<br/>Build-time only| CocoaPods

%% Styling
classDef thisRepo fill:#e0f2f1,stroke:#00796b,stroke-width:3px
classDef external fill:#ffe1e1,stroke:#d32f2f,stroke-width:2px,stroke-dasharray: 5 5
classDef database fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

class ThisRepo thisRepo
class FCM,APNs,Azure,CocoaPods external
class LocalDB database
```

## External Integrations

| External Service | Communication Type | Purpose |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe have a line like in https://github.com/OutSystems/cordova-outsystems-secure-sqlite-bundle/pull/48/changes#diff-8f6366fd8e7a5fa30f7f05879999195b7cf5fa1e3d157822eb37f0e91d226cfd - For the OutSystems Plugin that consumes this plugin, which in turn can be used in Mobile Apps? But I guess that's not really a service per-se. Maybe MABS? But that is kind of exterior and out of control from this repo.

|------------------|-------------------|---------|
| Firebase Cloud Messaging (FCM) | Sync (HTTP/gRPC) | Send/receive push notifications, manage device tokens, subscribe to topics |
| Apple Push Notification Service (APNs) | Sync (Native iOS API) | iOS-specific push notification delivery and APNs token management |
| Local SQLite/Room Database | Sync (SQL) | Store pending notifications locally for retrieval when app is offline or in background |
| OutSystems Azure Artifact Repository | Build-time (Maven) | Fetch proprietary Android AAR libraries (osfirebasemessaging-android, oslocalnotifications-android) |
| CocoaPods CDN | Build-time (CocoaPods) | Fetch Firebase iOS SDK (FirebaseMessaging 10.29.0) |

## Architectural Tenets

### T1. Platform-Specific Implementation with Shared Interface Contract

The plugin maintains separate native implementations for iOS and Android but exposes an identical JavaScript API surface. Each platform implements the same set of methods defined in the JavaScript bridge, ensuring host applications can use the plugin uniformly across platforms while delegating to platform-appropriate FCM SDKs.

**Evidence:**
- `www/OSFirebaseCloudMessaging.js` - Defines common JavaScript API with methods like `getToken`, `subscribe`, `registerDevice`
- `src/ios/OSFirebaseCloudMessaging.swift` (in `OSFirebaseCloudMessaging` class) - Swift implementation delegates to `FirebaseMessagingController` from iOS framework
- `src/android/com/outsystems/firebase/cloudmessaging/OSFirebaseCloudMessaging.kt` (in `OSFirebaseCloudMessaging` class) - Kotlin implementation delegates to `FirebaseMessagingController` from Android AAR
- Both platforms implement identical method signatures (`getToken`, `subscribe`, `unsubscribe`, `registerDevice`) despite using different underlying SDKs

### T2. Abstraction Through Proprietary Native Libraries

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs mention proprietary here a few times, and I'm not sure if that's really the case. They're not open source I guess, but that doesn't make them proprietary, as they can be technically used in other projects (although we don't support it)? Thinking if internal makes more sense. Maybe it's a nitpick, idk xD


Business logic and Firebase SDK interaction are isolated in proprietary native libraries (`OSFirebaseMessagingLib` for iOS, `osfirebasemessaging-android` for Android), not in this plugin's code. This plugin acts as a thin bridge layer that translates Cordova calls to library calls. The proprietary libraries are maintained separately and distributed as compiled artifacts.

**Evidence:**
- `plugin.xml` - References `OSFirebaseMessagingLib.xcframework` (lines 43-44) and `OSLocalNotificationsLib.xcframework` as embedded frameworks
- `src/android/com/outsystems/firebase/cloudmessaging/build.gradle` - Imports `com.github.outsystems:osfirebasemessaging-android:1.3.1@aar` from Azure repository
- `src/ios/OSFirebaseCloudMessaging.swift` (in `pluginInitialize`) - Instantiates `FirebaseMessagingController()` from external framework
- `src/android/com/outsystems/firebase/cloudmessaging/OSFirebaseCloudMessaging.kt` (in `initialize`) - Creates `FirebaseMessagingController` and `FirebaseNotificationManager` from imported AAR

### T3. Event-Driven Callback Pattern with Queue Buffering

The plugin uses an event listener system on the JavaScript side to handle asynchronous FCM events (notification clicks, token refresh). Events triggered before the Cordova device is ready are queued and replayed once the WebView is fully initialized, ensuring no events are lost during app startup.

**Evidence:**
- `www/OSFirebaseCloudMessaging.js` (in `on` and `un` functions) - Implements listener registration/deregistration pattern
- `www/OSFirebaseCloudMessaging.js` (in `fireQueuedEvents`) - Calls native `ready` action to flush queued events
- `src/ios/OSFirebaseCloudMessaging.swift` (in `ready` and `trigger` methods) - Maintains `eventQueue` array and `deviceReady` flag
- `src/android/com/outsystems/firebase/cloudmessaging/OSFirebaseCloudMessaging.kt` (in `ready` and `callbackNotifyApp`) - Uses `eventQueue: MutableList<String>` to buffer JavaScript calls

### T4. Build-Time Hooks for Platform-Specific Configuration

The plugin relies on Cordova/Capacitor build hooks to inject platform-specific configurations that cannot be expressed in static plugin descriptors. Hooks copy notification channel metadata, unzip custom sound files, modify AppDelegate/build.gradle files, and adapt to different build environments (Cordova vs. Capacitor).

**Evidence:**
- `plugin.xml` - Declares hooks: `after_prepare` hooks for `unzipSound.js`, `cleanUp.js`, `androidCopyChannelInfo.js`, `iOSCopyPreferences.js`
- `hooks/android/androidCopyChannelInfo.js` - Reads notification channel preferences from `config.xml` and writes to Android strings resource
- `hooks/unzipSound.js` - Extracts `sounds.zip` to platform-specific resource directories during build
- `build-actions/capacitor_hooks_update_after.js` (in `updateAppDelegate` function) - Injects Firebase delegate calls into iOS AppDelegate for Capacitor apps
- `package.json` - Defines `capacitor:update:after` script to handle Capacitor-specific build modifications

### T5. Permission Abstraction with Version-Aware Logic

The plugin abstracts runtime permission handling across different Android API levels, treating pre-Tiramisu (API 33) and post-Tiramisu behavior uniformly. iOS permission handling is always asynchronous. This allows consuming apps to call `registerDevice` without platform-specific conditional logic.

**Evidence:**
- `src/android/com/outsystems/firebase/cloudmessaging/OSFirebaseCloudMessaging.kt` (in `registerDevice`) - Checks `Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU` to determine if POST_NOTIFICATIONS permission is required
- `src/android/com/outsystems/firebase/cloudmessaging/OSFirebaseCloudMessaging.kt` (in `registerDevice`) - Uses `MutableSharedFlow` to convert Cordova permission callback to suspend function
- `src/android/com/outsystems/firebase/cloudmessaging/OSFCMPermissionEvents.kt` - Sealed class models permission result states (`Granted`, `NotGranted`)
- `src/ios/OSFirebaseCloudMessaging.swift` (in `registerDevice`) - Always calls `requestAuthorisation()` and checks result asynchronously

## Current Phase Constraints

### Capacitor Migration Strategy

The plugin currently supports both Cordova and Capacitor build systems using conditional logic. Cordova apps use method swizzling in AppDelegate, while Capacitor apps require hook-injected code modifications.

**Evidence:**
- `src/ios/AppDelegate+OSFirebaseCloudMessaging.m` - Uses runtime method swizzling but returns early if Capacitor bridge is detected
- `build-actions/capacitor_hooks_update_after.js` - Injects AppDelegate modifications specifically for Capacitor apps

**Expires when:** Cordova support is officially deprecated and all consuming apps migrate to Capacitor.
Comment on lines +107 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the evidence is true, but I debate whether or not this is a constraint. It's just in difference between how Cordova and Capacitor apps are structured.

And for a regular Capacitor app, it would me a matter of documenting what is necessary in AppDelegate and users could just add that themselves (as per the philosophy of Capacitor). But since this is for low-code apps, that's not really an option, hence the hook.

And since deprecating Cordova support entirely has no date in sight, it's not like we'll be getting rid of this soon. So either we can leave this section here but change it to Capacitor vs Cordova, or remove it altogether.

32 changes: 27 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

The changes documented here do not include those from the original repository.

### [2.5.6]
## [2.6.2]

### Chores

- chore(koda): initialize Koda (#133)

## [2.6.1]

### Chores

- chore: update dependency to OSFirebaseMessagingLib-Android (#139)

## [2.6.0]

### Features

- (ios) Support for Swift Package Manager (https://outsystemsrd.atlassian.net/browse/RMET-5137)

### Fixes

- (android) Avoid overwriting other plugins' `ext.postBuildExtras` Gradle configuration (https://outsystemsrd.atlassian.net/browse/RMET-5211)

## [2.5.6]

### Fixes

- (android) Handle errors in `RegisterDevice` instead of hanging indefinitely (https://outsystemsrd.atlassian.net/browse/RMET-4953)

### [2.5.5]
## [2.5.5]

### Fixes

- (android) Remove unnecessary dependencies to `oscore` and `oscordova` (https://outsystemsrd.atlassian.net/browse/RMET-4900)

### [2.5.4]
## [2.5.4]

### Fixes

- (android) Fix capacitor builds by removing kapt dependency (https://outsystemsrd.atlassian.net/browse/RMET-4862)

### [2.5.3]
## [2.5.3]

### Fixes

- (android) Fixes missing strings.xml on target project (https://outsystemsrd.atlassian.net/browse/RMET-4774)

### [2.5.2]
## [2.5.2]

### Fixes

Expand Down
156 changes: 156 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

This is a Cordova/Capacitor plugin that bridges Firebase Cloud Messaging (FCM) to hybrid mobile applications. The plugin consists of native code (Swift for iOS, Kotlin for Android) with a JavaScript API layer exposed to the WebView.

See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed architectural tenets, external integrations, and the Cordova/Capacitor migration strategy.

See [CONTRIBUTING.md](./CONTRIBUTING.md) for development workflow, branch naming conventions, commit format, and PR process.

## Directory Structure

```
src/ios/ - Swift/Objective-C bridge code
OSFirebaseCloudMessaging.swift - Main iOS plugin class
OSFCMEventExtensions.swift - Event handling utilities
AppDelegate+OSFirebaseCloudMessaging.{h,m} - Method swizzling for Cordova
frameworks/ - Proprietary xcframeworks (OSFirebaseMessagingLib, OSLocalNotificationsLib)

src/android/ - Kotlin bridge code
com/outsystems/firebase/cloudmessaging/
OSFirebaseCloudMessaging.kt - Main Android plugin class
OSFCMPermissionEvents.kt - Permission result states
build.gradle - Android dependencies (references osfirebasemessaging-android AAR)

www/ - JavaScript API exposed to WebView
OSFirebaseCloudMessaging.js - Cordova exec bridge

hooks/ - Cordova build hooks (run on after_prepare)
unzipSound.js - Extract sound resources
cleanUp.js - Remove temporary files
android/androidCopyChannelInfo.js - Copy notification channel config to strings.xml
ios/iOSCopyPreferences.js - Copy iOS preferences to plist

build-actions/ - Capacitor/ODC support (replaces hooks for Capacitor apps)
updateCloudMessagingConfigs.yaml - Build actions for channel config and entitlements
capacitor_hooks_update_after.js - AppDelegate injection for Capacitor
README.md - Build actions usage documentation

plugin.xml - Cordova plugin manifest (dependencies, hooks, platform configs)
```

## Testing and Development

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless claude is going to create their own apps when testing (unlikely unless explicitly requested), the way to go here is to manually test using OutSystems apps, particularly, the Firebase Sample App, which is publicly available on Forge. Maybe it's simpler to just state that instead?


This is a plugin, not a standalone application. To test changes:

### For Cordova Projects
```bash
# In your test Cordova app
cordova plugin add /absolute/path/to/cordova-outsystems-firebase-cloud-messaging
cordova build android
cordova build ios
```

### For Capacitor Projects
```bash
# In your test Capacitor app
npm install /absolute/path/to/cordova-outsystems-firebase-cloud-messaging
npx cap sync
```

The `capacitor:update:after` hook runs automatically during `cap sync` to inject AppDelegate modifications.

### Manually Trigger Capacitor Hook
```bash
npm run capacitor:update:after
```

## Plugin API Surface

The JavaScript API in `www/OSFirebaseCloudMessaging.js` exposes these methods:

- `getToken()` - Get FCM device token
- `getAPNsToken()` - Get APNs token (iOS only)
- `subscribe(topic)` / `unsubscribe(topic)` - Manage topic subscriptions
- `registerDevice()` / `unregisterDevice()` - Handle push permission and registration
- `getPendingNotifications(clearFromDatabase)` - Retrieve queued notifications
- `clearNotifications()` - Clear notification center
- `setBadge(badge)` / `getBadge()` - Badge management
- `sendLocalNotification(...)` - Trigger local notification
- `setDeliveryMetricsExportToBigQuery(enable)` - Configure BigQuery export
- `on(event, callback)` / `un(event, callback)` - Event listener registration

All methods use the Cordova exec bridge. Events are queued until device ready (see [ARCHITECTURE.md](./ARCHITECTURE.md) T3: Event-Driven Callback Pattern).

## Key Dependencies

### iOS (specified in plugin.xml)
- `FirebaseMessaging` pod version 10.29.0 (from CocoaPods CDN)
- `OSFirebaseMessagingLib.xcframework` (proprietary, embedded in repo)
- `OSLocalNotificationsLib.xcframework` (proprietary, embedded in repo)

### Android (specified in src/android/.../build.gradle)
- `com.github.outsystems:osfirebasemessaging-android:1.3.1@aar` (from OutSystems Azure)
- `com.github.outsystems:oslocalnotifications-android` (from OutSystems Azure)

The proprietary libraries contain the actual FCM SDK integration logic. This plugin acts as a thin bridge.

## Important Context

### Cordova vs Capacitor Build Systems

The plugin supports both build systems using conditional logic:

- **Cordova apps** use method swizzling in `AppDelegate+OSFirebaseCloudMessaging.m` and standard Cordova hooks
- **Capacitor apps** skip swizzling (detected via bridge check) and use `build-actions/capacitor_hooks_update_after.js` to inject AppDelegate code

See [ARCHITECTURE.md](./ARCHITECTURE.md) "Current Phase Constraints: Capacitor Migration Strategy" for details.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the other comment I made for "Current Phase Constraints: Capacitor Migration Strategy", this may need updates.


### Build Hooks Behavior

Hooks in `hooks/` directory run during `cordova prepare` or `cordova build`:
- `unzipSound.js` - Extracts `sounds.zip` to platform resource directories
- `androidCopyChannelInfo.js` - Reads notification channel preferences from `config.xml` and writes to Android `strings.xml`
- `iOSCopyPreferences.js` - Copies iOS preferences from `config.xml` to `.plist` files

For Capacitor apps, equivalent functionality is in `build-actions/updateCloudMessagingConfigs.yaml` (used by ODC Plugin Manager).

### Permission Handling

- **Android**: Pre-Tiramisu (API < 33) does not require POST_NOTIFICATIONS permission; Tiramisu+ does
- **iOS**: Always requires asynchronous permission request via `requestAuthorisation()`

The `registerDevice()` method abstracts this logic so consuming apps don't need platform-specific code (see [ARCHITECTURE.md](./ARCHITECTURE.md) T5: Permission Abstraction).

### Event Queue Pattern

Events triggered before Cordova's `deviceready` are buffered in native code (`eventQueue` on both platforms) and replayed when the JavaScript calls the `ready()` action. This ensures no notifications are lost during app startup (see [ARCHITECTURE.md](./ARCHITECTURE.md) T3).

## Common Tasks

### Update iOS Firebase Dependency
Edit `plugin.xml` line 54 to change the FirebaseMessaging pod version.

### Update Android Proprietary Library Version
Edit `src/android/com/outsystems/firebase/cloudmessaging/build.gradle` to change the AAR version.

### Add New JavaScript API Method
1. Add method to `www/OSFirebaseCloudMessaging.js` using `exec()`
2. Implement in `src/ios/OSFirebaseCloudMessaging.swift` (method name must match)
3. Implement in `src/android/.../OSFirebaseCloudMessaging.kt` (method name must match)
Comment on lines +140 to +143

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • for new API method, usually this will require implementation in the internal native libraries, that the contributor (depending on who the contributor is) may or may not have access to. And if not they should probably contact the team via internal channels or OutSystems if external?


### Debug Hook Execution
Hooks write to console during build. Check Cordova build output for hook logs. For Capacitor, check `build-actions/capacitor_hooks_update_after.js` console output during `cap sync`.

## Version and Release Management

When releasing a new version:
1. Update `version` in `package.json`
2. Update `version` attribute in `plugin.xml` root `<plugin>` element
3. Update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/) format
4. Commit with: `chore(release): raise to version X.Y.Z`

See [CONTRIBUTING.md](./CONTRIBUTING.md) "Versioning" section for details.
Loading