@maccesar/titools 4.23.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/titools",
3
- "version": "4.23.0",
3
+ "version": "4.23.1",
4
4
  "description": "Titanium SDK skills and agents for AI coding assistants, including native game and sound synthesis modules",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -79,6 +79,26 @@ if (OS_ANDROID) {
79
79
 
80
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)`.
81
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
+
82
102
  ## 2. The payload decides how much of the module runs
83
103
 
84
104
  Android distinguishes a *notification message* from a *data message*, and the difference is not cosmetic.
@@ -151,11 +171,34 @@ Everything above is what the published module needs. A module that fires `didOpe
151
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.
152
172
 
153
173
  ```javascript
154
- modulo.addEventListener('didOpenNotification', (e) => {
155
- abrirPanel((e.message && e.message.data) || {})
156
- })
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
+ }
157
198
  ```
158
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
+
159
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.
160
203
 
161
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.
@@ -249,7 +292,15 @@ Verified case by case on an iPad. The third row is the one that does not work on
249
292
 
250
293
  ```javascript
251
294
  Ti.Network.registerForPushNotifications({
252
- success: (e) => { enviarTokenAlServidor(e.deviceToken) },
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
+ },
253
304
  error: (e) => { log.error(e.error) },
254
305
  callback: (e) => {
255
306
  // Foreground, background and closed all arrive here.
@@ -274,6 +325,16 @@ if (OS_IOS) {
274
325
  }
275
326
  ```
276
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
+
277
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:
278
339
 
279
340
  ```php
@@ -323,3 +384,6 @@ Do not test with the app added to the battery whitelist. That measures a phone y
323
384
  | iOS registers but never gets a token | Firebase loaded after APNs answered | Section 8 |
324
385
  | iOS receives the push and does nothing when tapped | Empty `callback` | Section 8 |
325
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 |