@maccesar/titools 4.22.0 → 4.23.1
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/package.json +1 -1
- package/skills/purgetss/SKILL.md +1 -1
- package/skills/purgetss/references/app-branding.md +38 -4
- package/skills/purgetss/references/cli-commands.md +3 -2
- package/skills/purgetss/references/customization-deep-dive.md +3 -2
- package/skills/purgetss/references/version-history.md +4 -0
- package/skills/ti-expert/references/push-notifications.md +88 -7
package/package.json
CHANGED
package/skills/purgetss/SKILL.md
CHANGED
|
@@ -468,7 +468,7 @@ Load these only when needed:
|
|
|
468
468
|
- [PurgeTSS UI in Classic](references/purgetss-ui-classic.md) - Native JavaScript setup, public exports, animation objects, cleanup, and platform notes
|
|
469
469
|
|
|
470
470
|
### Release Notes
|
|
471
|
-
- [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.17.
|
|
471
|
+
- [Version History](references/version-history.md) - Release-by-release feature additions and behavior changes (v7.4.0 → v7.17.1)
|
|
472
472
|
|
|
473
473
|
> **💡 TEXT FONTS (Google Fonts, Roboto, etc.)**
|
|
474
474
|
> For text fonts, see [Custom Fonts](references/custom-fonts.md).
|
|
@@ -25,6 +25,7 @@ For the terse flag reference, see the [`brand` command reference](./cli-commands
|
|
|
25
25
|
- [Android dark mode](#android-dark-mode)
|
|
26
26
|
- [Android 12+ splash artwork](#android-12-splash-artwork)
|
|
27
27
|
- [Android pre-12 splash](#android-pre-12-splash)
|
|
28
|
+
- [FCM notification icon](#fcm-notification-icon)
|
|
28
29
|
- [The iOS launch screen and LaunchLogo.png](#the-ios-launch-screen-and-launchlogopng)
|
|
29
30
|
- [iPhone launch images](#iphone-launch-images)
|
|
30
31
|
- [iOS 18+ Dark and Tinted variants](#ios-18-dark-and-tinted-variants)
|
|
@@ -164,9 +165,10 @@ brand: {
|
|
|
164
165
|
appicon: { padding: '10%' }, // appicon.png (128×128)
|
|
165
166
|
androidSplash: { padding: '26%' }, // assets/android/default.png + images/res-*/default.png × 11
|
|
166
167
|
|
|
167
|
-
// Opt-in: inert until
|
|
168
|
+
// Opt-in: splash_icon stays inert until the Android theme points at it.
|
|
169
|
+
// notificationicon is read by firebase.cloudmessaging, by that exact name.
|
|
168
170
|
splashIcon: { enabled: false }, // drawable-*/splash_icon.png × 5
|
|
169
|
-
notificationIcon: { enabled: false }, // drawable-*/
|
|
171
|
+
notificationIcon: { enabled: false }, // drawable-*/notificationicon.png × 5
|
|
170
172
|
ninePatch: { enabled: false } // background.9.png (not implemented yet)
|
|
171
173
|
}
|
|
172
174
|
```
|
|
@@ -258,12 +260,12 @@ And these live at the top level:
|
|
|
258
260
|
| `appicon` | `appicon` | `appicon.png` (128×128) | `10%` | yes |
|
|
259
261
|
| `android-splash` | `androidSplash` | `assets/android/default.png` + `images/res-*/default.png` × 11 | `26%` | yes |
|
|
260
262
|
| `splash-icon` | `splashIcon` | `drawable-*/splash_icon.png` × 5 | — | `--splash-icon` |
|
|
261
|
-
| `notification-icon` | `notificationIcon` | `drawable-*/
|
|
263
|
+
| `notification-icon` | `notificationIcon` | `drawable-*/notificationicon.png` × 5 | — | `--notification-icon` |
|
|
262
264
|
| `nine-patch` | `ninePatch` | `background.9.png` | — | `--nine-patch` (not implemented yet) |
|
|
263
265
|
|
|
264
266
|
`ic_launcher.xml` always travels inside `adaptive`; it is never generated on its own.
|
|
265
267
|
|
|
266
|
-
|
|
268
|
+
Three pieces are opt-in, because none of them does anything on its own. `splash_icon.png` is inert until the theme points `windowSplashScreenAnimatedIcon` at it. `notificationicon.png` needs `firebase.cloudmessaging` in the project, plus a `meta-data` entry for the notification messages that do not reach it by name — see [FCM notification icon](#fcm-notification-icon). `background.9.png` is not implemented yet.
|
|
267
269
|
|
|
268
270
|
### `background` is inherited, `padding` is not
|
|
269
271
|
|
|
@@ -497,6 +499,34 @@ Earlier versions regenerated only the first file and hid the other 11 behind a `
|
|
|
497
499
|
> A solid `windowBackground` wins
|
|
498
500
|
> If the launch theme sets `android:windowBackground` to a plain color — which is what [Matching the launch background](#matching-the-launch-background) recommends — that color takes precedence over this artwork on Android <12. Drop the `windowBackground` item if you want the image to show instead.
|
|
499
501
|
|
|
502
|
+
## FCM notification icon
|
|
503
|
+
|
|
504
|
+
`notification-icon` is off by default. Turn it on with `brand.notificationIcon.enabled`, or for a single run with `--notification-icon`. It writes five densities to `app/platform/android/res/drawable-*/notificationicon.png` (`platform/android/res/drawable-*/notificationicon.png` in Classic), white on transparent, because **Android tints status-bar icons at runtime**: every non-transparent pixel comes out white and the color in the artwork is discarded.
|
|
505
|
+
|
|
506
|
+
The filename is fixed, like `appicon.png` and `DefaultIcon.png`, because the consumer dictates it. `firebase.cloudmessaging` resolves the drawable by name inside `TiFirebaseMessagingService.showNotification()`, through `getResource("notificationicon")`, and that lookup is the only path a **data** message has. When the drawable is missing the module falls back to `appicon` — the opaque launcher icon, which the status bar renders as a white blob, since only the alpha channel survives the tint.
|
|
507
|
+
|
|
508
|
+
**Notification** messages take the configurable path instead. Two `meta-data` entries go under `<application>` in `tiapp.xml`:
|
|
509
|
+
|
|
510
|
+
```xml
|
|
511
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_icon"
|
|
512
|
+
android:resource="@drawable/notificationicon"/>
|
|
513
|
+
<meta-data android:name="com.google.firebase.messaging.default_notification_color"
|
|
514
|
+
android:resource="@color/notification_tint"/>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
The second one colors the notification's accent, not the icon. Create or merge `app/platform/android/res/values/colors.xml` (`platform/android/res/values/colors.xml` in Classic):
|
|
518
|
+
|
|
519
|
+
```xml
|
|
520
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
521
|
+
<resources>
|
|
522
|
+
<color name="notification_tint">#0B1326</color>
|
|
523
|
+
</resources>
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
> **Renamed in v7.17.1**
|
|
527
|
+
>
|
|
528
|
+
> The piece used to write `ic_stat_notify.png`, the Android convention for status-bar drawables but not the name the module looks up. Under the old name a data message never found the icon, whatever the `meta-data` said. A project that wired `@drawable/ic_stat_notify` by hand changes that one line and deletes the five stale files.
|
|
529
|
+
|
|
500
530
|
## The iOS launch screen and LaunchLogo.png
|
|
501
531
|
|
|
502
532
|
Titanium builds `LaunchLogo.imageset` itself on every iOS build, resizing one source into the five sizes it needs. It looks for `LaunchLogo.png` first and falls back to `DefaultIcon.png`.
|
|
@@ -744,6 +774,10 @@ cp docs/my-logo-mono.svg purgetss/brand/logo-mono.svg
|
|
|
744
774
|
purgetss brand
|
|
745
775
|
```
|
|
746
776
|
|
|
777
|
+
### The notification shows a white square, or the app icon instead of my notification icon
|
|
778
|
+
|
|
779
|
+
`firebase.cloudmessaging` did not find the drawable and fell back to `appicon`, which the status bar tints into a solid blob. Three causes, in order of likelihood: the piece was never generated (`brand.notificationIcon.enabled` is `false` by default — run `purgetss brand --only notification-icon`), the project still carries `ic_stat_notify.png` from before v7.17.1, or the `meta-data` entry points at the old name. See [FCM notification icon](#fcm-notification-icon).
|
|
780
|
+
|
|
747
781
|
### iOS rejects the app icon upload ("contains transparency")
|
|
748
782
|
|
|
749
783
|
Apple requires App Store icons to have no alpha channel. `DefaultIcon-ios.png` is always flattened on `brand.background` for that reason. If you edited the file manually and reintroduced alpha, re-run `purgetss brand`.
|
|
@@ -82,7 +82,8 @@ module.exports = {
|
|
|
82
82
|
adaptive: { padding: '18%' }, legacyIcon: { padding: '10%' },
|
|
83
83
|
appicon: { padding: '10%' }, androidSplash: { padding: '26%' },
|
|
84
84
|
|
|
85
|
-
// Opt-in: inert until
|
|
85
|
+
// Opt-in: splash_icon stays inert until the Android theme points at it.
|
|
86
|
+
// notificationicon is read by firebase.cloudmessaging, by that exact name.
|
|
86
87
|
splashIcon: { enabled: false }, notificationIcon: { enabled: false },
|
|
87
88
|
ninePatch: { enabled: false }
|
|
88
89
|
},
|
|
@@ -236,7 +237,7 @@ Radius precedence and the outputs intentionally left unmasked are documented in
|
|
|
236
237
|
|
|
237
238
|
| Flag | Purpose |
|
|
238
239
|
| --- | --- |
|
|
239
|
-
| `--notification-icon` | Also emit `
|
|
240
|
+
| `--notification-icon` | Also emit `notificationicon.png × 5`. |
|
|
240
241
|
| `--splash-icon` | Also emit `splash_icon.png × 5`. |
|
|
241
242
|
| `--nine-patch` | Declared but not implemented yet; prints a warning and writes nothing. |
|
|
242
243
|
**Logo variants & overrides**
|
|
@@ -70,9 +70,10 @@ module.exports = {
|
|
|
70
70
|
appicon: { padding: '10%' }, // appicon.png (128×128)
|
|
71
71
|
androidSplash: { padding: '26%' }, // assets/android/default.png + images/res-*/default.png × 11
|
|
72
72
|
|
|
73
|
-
// Opt-in: inert until
|
|
73
|
+
// Opt-in: splash_icon stays inert until the Android theme points at it.
|
|
74
|
+
// notificationicon is read by firebase.cloudmessaging, by that exact name.
|
|
74
75
|
splashIcon: { enabled: false }, // drawable-*/splash_icon.png × 5
|
|
75
|
-
notificationIcon: { enabled: false }, // drawable-*/
|
|
76
|
+
notificationIcon: { enabled: false }, // drawable-*/notificationicon.png × 5
|
|
76
77
|
ninePatch: { enabled: false } // background.9.png (not implemented yet)
|
|
77
78
|
},
|
|
78
79
|
// Sources in purgetss/images/ are 4x masters: a 1024px file yields
|
|
@@ -8,6 +8,10 @@ When in doubt about whether a class, flag, or config key exists in the user's in
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## v7.17.1
|
|
12
|
+
- **The `notification-icon` piece writes `notificationicon.png`, not `ic_stat_notify.png`.** `firebase.cloudmessaging` hardcodes that name — `TiFirebaseMessagingService.showNotification()` calls `getResource("notificationicon")` — and it is the only path a **data** message has. The `default_notification_icon` meta-data is the configurable one and covers **notification** messages alone, so under the old name no manifest entry could rescue a data message: the module fell back to the opaque `appicon`, which the status bar renders as a white blob. The filename is fixed rather than a config key, for the same reason `appicon.png` and `DefaultIcon.png` are. Projects that wired `@drawable/ic_stat_notify` by hand need to update that one meta-data line and delete the five stale files. See [app-branding.md](app-branding.md).
|
|
13
|
+
- The comment above the opt-in pieces in the generated config now names each consumer instead of claiming both are inert without hand-edited XML. Only `splash_icon` is.
|
|
14
|
+
|
|
11
15
|
## v7.17.0
|
|
12
16
|
- The `images:` section rejects unknown keys instead of ignoring them, at both levels: the five top-level keys (`quality`, `format`, `autoSync`, `confirmOverwrites`, `files`) and the three inside each `files[]` entry (`filename`, `width`, `height`, with `filename` required). Nothing is generated when validation fails. Never suggest `width`, `opacity`, `padding` or `output` as config keys — they are CLI flags and now abort the run. See [multi-density-images.md](multi-density-images.md).
|
|
13
17
|
- The generated `images:` block gained four comment lines explaining the 4× master convention, and the `quality` comment now names the formats it reaches: `webp`, `jpeg`, `avif`, `tiff`. PNG uses `compressionLevel: 9` and GIF takes no quality parameter, so neither is affected. Both copies changed — the one patched into an existing config and the one `init` seeds.
|
|
@@ -11,7 +11,6 @@ This file is about the community module [`firebase.cloudmessaging`](https://gith
|
|
|
11
11
|
<!-- TOC-START -->
|
|
12
12
|
## Contents
|
|
13
13
|
|
|
14
|
-
- [Contents](#contents)
|
|
15
14
|
- [1. The four doors](#1-the-four-doors)
|
|
16
15
|
- [2. The payload decides how much of the module runs](#2-the-payload-decides-how-much-of-the-module-runs)
|
|
17
16
|
- [3. A tap does not fire `didReceiveMessage`](#3-a-tap-does-not-fire-didreceivemessage)
|
|
@@ -34,7 +33,7 @@ This file is about the community module [`firebase.cloudmessaging`](https://gith
|
|
|
34
33
|
| Backgrounded, person taps the notification | `Ti.App` `resumed`, then read the Intent | nothing |
|
|
35
34
|
| **Already in front**, person taps a notification sitting in the tray | `newintent` on the root activity | `launchMode="singleTop"` |
|
|
36
35
|
|
|
37
|
-
A module that fires `didOpenNotification` replaces rows
|
|
36
|
+
A module that fires `didOpenNotification` replaces rows three and four with one listener. Row two still arrives in the Intent, because the module does not exist yet when the notification is opened. See [If your module fires `didOpenNotification`](#if-your-module-fires-didopennotification).
|
|
38
37
|
|
|
39
38
|
Verified case by case on a physical Android phone, reading a log line that named which door each message came through rather than watching the screen.
|
|
40
39
|
|
|
@@ -80,6 +79,26 @@ if (OS_ANDROID) {
|
|
|
80
79
|
|
|
81
80
|
**Wrap the listeners.** `Ti.App.addEventListener('resumed', mostrarAviso)` passes the event object as the first argument, and if the function's first parameter is an Intent, the code blows up inside `getStringExtra`. The same trap catches `Alloy.Events.on('algo', unaFuncionConParametros)`.
|
|
82
81
|
|
|
82
|
+
### Registering on Android: two calls that go quiet
|
|
83
|
+
|
|
84
|
+
The channel has to exist and be handed to the module **before** asking for the token, and its id has to match the `default_notification_channel_id` meta-data in `tiapp.xml`. Mismatch them and the notification is posted against a channel nobody declared.
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
modulo.notificationChannel = Ti.Android.NotificationManager.createNotificationChannel({
|
|
88
|
+
id: 'default_channel', // the same id tiapp.xml declares
|
|
89
|
+
name: L('push_channel_name'),
|
|
90
|
+
importance: Ti.Android.IMPORTANCE_DEFAULT
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
modulo.registerForPushNotifications()
|
|
94
|
+
|
|
95
|
+
// Firebase does not announce a token it already had, so didRefreshRegistrationToken
|
|
96
|
+
// may never fire on a device that registered before. Ask for it once, a moment later.
|
|
97
|
+
setTimeout(() => guardarToken(modulo.fcmToken), 4000)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
That last line is the one that bites. On a fresh install the event fires and everything looks fine; on a device that already had a token from an earlier run it never fires, the app never sends the token to the server, and every push to that device is addressed to nobody. Make `guardarToken` idempotent — it will be called more than once with the same value.
|
|
101
|
+
|
|
83
102
|
## 2. The payload decides how much of the module runs
|
|
84
103
|
|
|
85
104
|
Android distinguishes a *notification message* from a *data message*, and the difference is not cosmetic.
|
|
@@ -93,6 +112,12 @@ That last symptom is worth naming because it sends people down the wrong road. I
|
|
|
93
112
|
|
|
94
113
|
Firebase's own server documentation teaches the `notification` block, which is why this is such a common shape to arrive at.
|
|
95
114
|
|
|
115
|
+
### The icon, whichever route posts it
|
|
116
|
+
|
|
117
|
+
Both routes need a drawable named `notificationicon`, because the module resolves it by that exact name: `TiFirebaseMessagingService.showNotification()` calls `getResource("notificationicon")`, and that lookup is the only path a data message has. When it is missing the module falls back to `appicon` — the opaque launcher icon, which the status bar tints into a white square, since only the alpha channel survives. A notification message takes the configurable path instead, the `com.google.firebase.messaging.default_notification_icon` meta-data, which is why the same project can show a correct icon on one route and a blob on the other.
|
|
118
|
+
|
|
119
|
+
PurgeTSS generates the five densities, white on transparent and under the right name, with `purgetss brand --only notification-icon` — off by default. Its `app-branding.md` § "FCM notification icon" carries both meta-data entries and the `colors.xml` resource that `default_notification_color` needs.
|
|
120
|
+
|
|
96
121
|
## 3. A tap does not fire `didReceiveMessage`
|
|
97
122
|
|
|
98
123
|
When the module posts the notification and the person taps it, `PushHandlerActivity` puts the payload on the launcher Intent as the `fcm_data` extra and starts the activity. It does not fire `didReceiveMessage`.
|
|
@@ -134,20 +159,54 @@ Take the activity name from the built manifest rather than guessing it: `build/a
|
|
|
134
159
|
|
|
135
160
|
### If your module fires `didOpenNotification`
|
|
136
161
|
|
|
137
|
-
Everything above is what the published module needs. A module that fires `didOpenNotification` from `setNotificationData()`
|
|
162
|
+
Everything above is what the published module needs. A module that fires `didOpenNotification` from `setNotificationData()` drops the `singleTop`, the `newintent` listener and the Intent read on resume. Two routes remain instead of four, and they are exclusive:
|
|
163
|
+
|
|
164
|
+
| What the app was doing | Where the payload arrives |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| Open, in front, message arrives | `didReceiveMessage` |
|
|
167
|
+
| Backgrounded, person taps | `didOpenNotification` |
|
|
168
|
+
| Already in front, person taps a notification in the tray | `didOpenNotification` |
|
|
169
|
+
| Killed, person taps | `fcm_data` extra on the launch Intent |
|
|
170
|
+
|
|
171
|
+
Verified on a physical Android 15 phone and an API 35 emulator, each row confirmed by a log line naming the route. The first three ran back to back on one process id with a game in progress, which survived; only the fourth starts a new process.
|
|
138
172
|
|
|
139
173
|
```javascript
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
if (OS_ANDROID) {
|
|
175
|
+
// With the runtime alive the launcher Intent is not delivered a second time,
|
|
176
|
+
// so this event is the only way in.
|
|
177
|
+
modulo.addEventListener('didOpenNotification', (e) => {
|
|
178
|
+
abrirPanel((e.message && e.message.data) || {})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// On a cold start the module does not exist yet when the notification is
|
|
182
|
+
// opened, so the payload travels in the Intent. This is that route.
|
|
183
|
+
const desdeElIntent = (origen) => {
|
|
184
|
+
const contenido = Ti.Android.rootActivity.intent.getStringExtra('fcm_data')
|
|
185
|
+
|
|
186
|
+
if (!contenido) {
|
|
187
|
+
log.debug(`Nothing in the Intent (${origen})`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
modulo.clearLastData()
|
|
192
|
+
abrirPanel(JSON.parse(contenido))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Ti.App.addEventListener('resumed', () => desdeElIntent('resumed'))
|
|
196
|
+
desdeElIntent('arranque')
|
|
197
|
+
}
|
|
143
198
|
```
|
|
144
199
|
|
|
200
|
+
`resumed` stays even though the event covers the warm tap: it is what catches an Intent the module left behind on a version that still writes one. When the event delivered the payload the Intent is empty and the handler returns without opening anything, which is the exclusivity the section below describes.
|
|
201
|
+
|
|
145
202
|
The payload sits under `message.data`, the same place `didReceiveMessage` puts it on both its live and its cold-start path, so one accessor covers a tap and an arrival. The first version of the event wrapped it directly in `message`; if the module you have predates 3.6.0, read `e.message` instead, or log the event once and look.
|
|
146
203
|
|
|
147
204
|
The Intent is still how a cold start arrives, so keep that path. Check `timodule.xml` in your installed module for the event, or just add the listener and watch whether it fires: a module without it ignores the registration silently.
|
|
148
205
|
|
|
149
206
|
Do not run both. Where the module also puts the payload on the Intent, `resumed` finds it again on Titanium's second pass and the panel opens twice. That is a real symptom, not a hypothetical: it shows up on Android 10 and not on Android 15, because the two versions differ in whether the launcher Intent gets delivered to a task that already exists.
|
|
150
207
|
|
|
208
|
+
To test the fourth row, swipe the app out of Recents. `adb shell am kill` looks like the same thing and is not: it kills the process but Android keeps the activity's saved state, and on restore Titanium puts `tiLaunchIntentExtras` from the previous launch back over the new Intent, taking `fcm_data` with it. The `TiRootActivity` checkpoint tells you which one you got — `savedInstanceState: null` is a real cold start, anything else is a restore. `adb shell am force-stop` is worse: it leaves the app in the stopped state, where Android stops delivering FCM messages until someone launches it by hand.
|
|
209
|
+
|
|
151
210
|
## 5. Why two splash screens flash by
|
|
152
211
|
|
|
153
212
|
Tap a notification and you may see one or two blank pages slide past before the app appears, and the splash icon never finishes animating. Opening the same app from the launcher icon looks fine.
|
|
@@ -233,7 +292,15 @@ Verified case by case on an iPad. The third row is the one that does not work on
|
|
|
233
292
|
|
|
234
293
|
```javascript
|
|
235
294
|
Ti.Network.registerForPushNotifications({
|
|
236
|
-
success: (e) => {
|
|
295
|
+
success: (e) => {
|
|
296
|
+
// e.deviceToken is the APNs one. It is not what you send to a server that
|
|
297
|
+
// pushes through FCM, and Firebase picks it up on its own — see below.
|
|
298
|
+
log.info(`APNs token: ${e.deviceToken ? 'received' : 'EMPTY'}`)
|
|
299
|
+
|
|
300
|
+
modulo.fetchToken((r) => {
|
|
301
|
+
if (r && r.token) { guardarToken(r.token) }
|
|
302
|
+
})
|
|
303
|
+
},
|
|
237
304
|
error: (e) => { log.error(e.error) },
|
|
238
305
|
callback: (e) => {
|
|
239
306
|
// Foreground, background and closed all arrive here.
|
|
@@ -258,6 +325,16 @@ if (OS_IOS) {
|
|
|
258
325
|
}
|
|
259
326
|
```
|
|
260
327
|
|
|
328
|
+
**The token comes from `fetchToken`, not from the property.** Right after APNs answers, `modulo.fcmToken` is empty: Firebase still has to exchange the APNs token for its own, and that exchange has no fixed duration. Waiting does not fix it — four seconds was still empty on a device. `fetchToken` calls back when the token exists, which is the only way to know.
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
// The apnsToken is never assigned by hand. Firebase's swizzling does it, and
|
|
332
|
+
// setting it yourself produces BadDeviceToken when you send.
|
|
333
|
+
modulo.fetchToken((e) => {
|
|
334
|
+
if (e && e.token) { guardarToken(e.token) }
|
|
335
|
+
})
|
|
336
|
+
```
|
|
337
|
+
|
|
261
338
|
**A data-only message shows nothing on iOS.** Android reads the text from `data`; iOS reads it from the APNs alert. Send both, they do not interfere:
|
|
262
339
|
|
|
263
340
|
```php
|
|
@@ -297,6 +374,7 @@ Do not test with the app added to the battery whitelist. That measures a phone y
|
|
|
297
374
|
| --- | --- | --- |
|
|
298
375
|
| Tapping restarts the app from zero and loses state | Top-level `notification` block; the system owns the intent | Section 2 |
|
|
299
376
|
| `image`, `big_text`, `channelId`, `sound`, `vibrate` ignored | Same | Section 2 |
|
|
377
|
+
| A white square or the launcher icon in the status bar | No `notificationicon` drawable; the module fell back to `appicon` | Section 2 |
|
|
300
378
|
| Foreground and cold start work, backgrounded tap does nothing | Nobody reads the Intent on resume | Section 3 |
|
|
301
379
|
| Nothing at all happens when tapping a stored notification while using the app | No `resumed`, no `newintent` listener, or no `singleTop` | Section 4 |
|
|
302
380
|
| Blank pages slide past before the app appears | `PushHandlerActivity` has no theme; Titanium spawns a duplicate root activity | Section 5 |
|
|
@@ -306,3 +384,6 @@ Do not test with the app added to the battery whitelist. That measures a phone y
|
|
|
306
384
|
| iOS registers but never gets a token | Firebase loaded after APNs answered | Section 8 |
|
|
307
385
|
| iOS receives the push and does nothing when tapped | Empty `callback` | Section 8 |
|
|
308
386
|
| Nothing arrives for twenty minutes, then five at once | OEM battery management | Section 9 |
|
|
387
|
+
| Push works on a fresh install and never on a reinstall | `didRefreshRegistrationToken` does not fire for a token Firebase already had | Section 1 |
|
|
388
|
+
| iOS: `fcmToken` is empty right after the permission is granted | It is; the APNs-for-FCM exchange has no fixed duration. Use `fetchToken` | Section 8 |
|
|
389
|
+
| iOS: `BadDeviceToken` when sending | `apnsToken` was assigned by hand instead of leaving it to the swizzling | Section 8 |
|