@maccesar/titools 4.3.0 → 4.4.2

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.
Files changed (41) hide show
  1. package/README.md +15 -12
  2. package/package.json +1 -1
  3. package/skills/purgetss/SKILL.md +4 -9
  4. package/skills/ti-api/references/api-core.md +24 -69
  5. package/skills/ti-api/references/api-data-network.md +18 -63
  6. package/skills/ti-api/references/api-media.md +15 -49
  7. package/skills/ti-api/references/api-modules-coremotion-urlsession.md +6 -20
  8. package/skills/ti-api/references/api-modules-map.md +17 -40
  9. package/skills/ti-api/references/api-modules-nfc.md +23 -56
  10. package/skills/ti-api/references/api-ui-android.md +16 -37
  11. package/skills/ti-api/references/api-ui-extras.md +28 -72
  12. package/skills/ti-api/references/api-ui-lists.md +14 -35
  13. package/skills/ti-api/references/api-ui-text-input.md +13 -39
  14. package/skills/ti-api/references/api-ui-windows-navigation.md +17 -43
  15. package/skills/ti-api/references/api-xml-global.md +25 -60
  16. package/skills/ti-expert/SKILL.md +35 -5
  17. package/skills/ti-expert/references/adaptive-layouts.md +15 -43
  18. package/skills/ti-expert/references/alloy-builtins.md +8 -22
  19. package/skills/ti-expert/references/alloy-structure.md +34 -18
  20. package/skills/ti-expert/references/anti-patterns.md +18 -39
  21. package/skills/ti-expert/references/code-conventions.md +5 -3
  22. package/skills/ti-expert/references/controller-patterns.md +31 -26
  23. package/skills/ti-expert/references/examples.md +41 -21
  24. package/skills/ti-expert/references/feedback-migration.md +143 -0
  25. package/skills/ti-expert/references/feedback-surfaces.md +111 -0
  26. package/skills/ti-expert/references/feedback-widget-contracts.md +175 -0
  27. package/skills/ti-expert/references/file-type-association.md +265 -0
  28. package/skills/ti-expert/references/migration-patterns.md +37 -25
  29. package/skills/ti-expert/references/security-device.md +58 -56
  30. package/skills/ti-expert/references/sharing.md +227 -0
  31. package/skills/ti-expert/references/state-management.md +5 -5
  32. package/skills/ti-expert/references/theming.md +109 -392
  33. package/skills/ti-guides/references/coding-best-practices.md +1 -2
  34. package/skills/ti-guides/references/style-and-conventions.md +1 -2
  35. package/skills/ti-howtos/references/debugging-profiling.md +1 -2
  36. package/skills/ti-howtos/references/location-and-maps.md +1 -2
  37. package/skills/ti-howtos/references/using-modules.md +1 -2
  38. package/skills/ti-howtos/references/web-content-integration.md +1 -2
  39. package/skills/ti-howtos/references/webpack-build-pipeline.md +1 -2
  40. package/skills/ti-ui/references/listviews-and-performance.md +5 -10
  41. package/skills/ti-ui/references/orientation.md +1 -2
@@ -105,6 +105,9 @@ const { TokenStorage } = require('services/tokenStorage')
105
105
  const { AuthService } = require('services/authService')
106
106
 
107
107
  function init() {
108
+ // The login view declares the app-owned Dialog Widget as its last child.
109
+ $.dialog.attach($.getView())
110
+
108
111
  // Check if biometric login is available and enabled
109
112
  const { available, type } = BiometricService.checkAvailability()
110
113
  const biometricEnabled = Ti.App.Properties.getBool('biometricEnabled', false)
@@ -143,22 +146,25 @@ async function onLoginSuccess(user) {
143
146
  if (available && !Ti.App.Properties.getBool('askedBiometric', false)) {
144
147
  Ti.App.Properties.setBool('askedBiometric', true)
145
148
 
146
- // Ask user if they want to enable biometric login
147
- const dialog = Ti.UI.createAlertDialog({
149
+ // The choice is app-owned; the later biometric authentication prompt remains native.
150
+ $.dialog.show({
148
151
  title: String.format(L('enable_biometric_title'), type),
149
152
  message: String.format(L('enable_biometric_msg'), type),
150
- buttonNames: [L('not_now'), L('enable')]
151
- })
152
-
153
- dialog.addEventListener('click', (e) => {
154
- if (e.index === 1) {
153
+ cancelTitle: L('not_now'),
154
+ confirmTitle: L('enable'),
155
+ onConfirm: () => {
155
156
  Ti.App.Properties.setBool('biometricEnabled', true)
156
157
  }
157
158
  })
158
-
159
- dialog.show()
160
159
  }
161
160
  }
161
+
162
+ function cleanup() {
163
+ $.dialog.destroy()
164
+ $.destroy()
165
+ }
166
+
167
+ $.cleanup = cleanup
162
168
  ```
163
169
 
164
170
  ## Deep link security
@@ -482,7 +488,9 @@ exports.SecurityService = {
482
488
  enforceSecurityPolicy() {
483
489
  const { compromised, reasons } = this.checkDeviceIntegrity()
484
490
 
485
- if (!compromised) return true
491
+ if (!compromised) {
492
+ return { outcome: 'allow', reasons: [], restricted: false }
493
+ }
486
494
 
487
495
  // Log security event
488
496
  logger.error('Security', 'Compromised device detected', {
@@ -496,50 +504,16 @@ exports.SecurityService = {
496
504
 
497
505
  switch (policy) {
498
506
  case 'block':
499
- // Prevent app usage
500
- this._showBlockedScreen()
501
- return false
507
+ return { outcome: 'block', reasons, restricted: true }
502
508
 
503
509
  case 'restrict':
504
- // Disable sensitive features
505
510
  Ti.App.Properties.setBool('restrictedMode', true)
506
- this._showWarning()
507
- return true
511
+ return { outcome: 'warn', reasons, restricted: true }
508
512
 
509
513
  case 'warn':
510
514
  default:
511
- // Just warn the user
512
- this._showWarning()
513
- return true
515
+ return { outcome: 'warn', reasons, restricted: false }
514
516
  }
515
- },
516
-
517
- _showBlockedScreen() {
518
- // Note: iOS guidelines prohibit programmatically terminating an app.
519
- // The best practice is to show a blocking dialog and prevent further interaction.
520
- // On Android you can call Ti.Android.currentActivity.finish() to close the activity.
521
- const dialog = Ti.UI.createAlertDialog({
522
- title: L('security_blocked_title'),
523
- message: L('security_blocked_msg'),
524
- buttonNames: [L('close')]
525
- })
526
-
527
- dialog.addEventListener('click', () => {
528
- if (OS_ANDROID) {
529
- Ti.Android.currentActivity.finish()
530
- }
531
- // On iOS: leave the dialog visible — Apple guidelines prohibit force-quitting apps.
532
- })
533
-
534
- dialog.show()
535
- },
536
-
537
- _showWarning() {
538
- Ti.UI.createAlertDialog({
539
- title: L('security_warning_title'),
540
- message: L('security_warning_msg'),
541
- buttonNames: [L('i_understand')]
542
- }).show()
543
517
  }
544
518
  }
545
519
  ```
@@ -550,18 +524,45 @@ exports.SecurityService = {
550
524
  // alloy.js
551
525
  const { SecurityService } = require('services/securityService')
552
526
 
553
- // Check device integrity at app start
554
- const securityCheck = SecurityService.enforceSecurityPolicy()
527
+ // Services return policy; they do not create UI before the root exists.
528
+ const securityDecision = SecurityService.enforceSecurityPolicy()
529
+ const root = Alloy.createController('index', { securityDecision })
530
+ root.getView().open()
531
+ ```
532
+
533
+ ```javascript
534
+ // controllers/index.js
535
+ const decision = $.args.securityDecision
536
+
537
+ function init() {
538
+ $.dialog.attach($.indexWindow)
539
+
540
+ if (decision.outcome === 'block') {
541
+ // Show a dedicated blocking state in the root XML and do not initialize
542
+ // protected application content. Do not force-quit the app.
543
+ $.blockingState.visible = true
544
+ $.appContent.visible = false
545
+ return
546
+ }
547
+
548
+ initializeAppContent({ restricted: decision.restricted })
549
+
550
+ if (decision.outcome === 'warn') {
551
+ $.dialog.show({
552
+ title: L('security_warning_title'),
553
+ message: L('security_warning_msg'),
554
+ tone: 'warning',
555
+ confirmTitle: L('i_understand')
556
+ })
557
+ }
558
+ }
555
559
 
556
- if (!securityCheck) {
557
- // Don't initialize the app
558
- return
560
+ function cleanup() {
561
+ $.dialog.destroy()
562
+ $.destroy()
559
563
  }
560
564
 
561
- // Check again periodically (in case of runtime jailbreak tools)
562
- setInterval(() => {
563
- SecurityService.checkDeviceIntegrity()
564
- }, 5 * 60 * 1000) // Every 5 minutes
565
+ $.cleanup = cleanup
565
566
  ```
566
567
 
567
568
  ## Security checklist
@@ -578,3 +579,4 @@ setInterval(() => {
578
579
  | **Integrity** | Check for jailbreak/root | checkDeviceIntegrity() |
579
580
  | **Integrity** | Define security policy | block/restrict/warn |
580
581
  | **Integrity** | Log security events | Always log compromises |
582
+ | **Integrity UI** | Keep policy out of services | Root controller presents blocking state or Dialog |
@@ -0,0 +1,227 @@
1
+ # Sharing content out
2
+
3
+ How to hand a link, a text or a file from your app to the rest of the system — the share sheet on iOS, the intent chooser on Android.
4
+
5
+ The two platforms are not symmetric and have not been for years, which is the single fact that explains every confusing implementation you will find in an older codebase. Android does this with the core SDK and no module. iOS does not: the SDK exposes no wrapper for `UIActivityViewController`, so a native module is still required. Code written before that split settled tends to route both platforms through the module, or through a cross-platform helper that only ever needed to exist for iOS.
6
+
7
+ This file is about sending content **out**. For receiving files — declaring a custom extension, UTIs, intent filters, `handleurl` — see [File type association](file-type-association.md).
8
+
9
+ ## Contents
10
+
11
+ 1. [The platform split](#1-the-platform-split)
12
+ 2. [Android: the core SDK is enough](#2-android-the-core-sdk-is-enough)
13
+ 3. [iOS: which module, and why one is still needed](#3-ios-which-module-and-why-one-is-still-needed)
14
+ 4. [Text is not a file](#4-text-is-not-a-file)
15
+ 5. [Do not pass text alongside a file](#5-do-not-pass-text-alongside-a-file)
16
+ 6. [iPad needs an anchor](#6-ipad-needs-an-anchor)
17
+ 7. [Knowing whether the user actually shared](#7-knowing-whether-the-user-actually-shared)
18
+ 8. [Failing without an exception](#8-failing-without-an-exception)
19
+ 9. [Verify instead of assuming](#9-verify-instead-of-assuming)
20
+ 10. [Symptom to cause](#10-symptom-to-cause)
21
+
22
+ ## 1. The platform split
23
+
24
+ | | Text or link | File |
25
+ | --- | --- | --- |
26
+ | **Android** | `Ti.Android.createIntent` + `createIntentChooser` | Same, with `putExtraUri` |
27
+ | **iOS** | Native module — no SDK API | Native module, or `DocumentViewer` if the type is previewable |
28
+
29
+ The asymmetry is historical, not architectural. Sharing on Titanium used to go through `com.alcoapps.socialshare`, a CommonJS wrapper by Ricardo Alcocer that split by platform and drove the same iOS module underneath. Once Android could open the system sheet directly, the Android branch stopped needing anything and the module became iOS-only — but the wrapper stayed in projects, and with it a layer of indirection that no longer buys anything.
30
+
31
+ **When you find one of those wrappers,** check what it still does. The usual finding is dead weight: version-comparison helpers, Facebook app-id variables, `image_blob` / `image_url` normalization for an argument shape nobody passes anymore. Adapting the old helper costs more than writing the two branches.
32
+
33
+ ## 2. Android: the core SDK is enough
34
+
35
+ `Ti.Android.createIntent` and `Ti.Android.createIntentChooser` are core SDK APIs and are not deprecated. No module, no widget.
36
+
37
+ ```javascript
38
+ function shareTextAndroid (text, chooserTitle) {
39
+ const activity = Ti.Android.rootActivity || Ti.Android.currentActivity
40
+
41
+ // No activity, no chooser to present. Calling startActivity on null throws in
42
+ // the face of whoever tapped Share.
43
+ if (!activity) {
44
+ return false
45
+ }
46
+
47
+ const intent = Ti.Android.createIntent({
48
+ action: Ti.Android.ACTION_SEND,
49
+ type: 'text/plain'
50
+ })
51
+
52
+ intent.putExtra(Ti.Android.EXTRA_TEXT, text)
53
+ activity.startActivity(Ti.Android.createIntentChooser(intent, chooserTitle))
54
+
55
+ return true
56
+ }
57
+ ```
58
+
59
+ For a file, two things change and both are mandatory:
60
+
61
+ ```javascript
62
+ intent.putExtraUri(Ti.Android.EXTRA_STREAM, file.nativePath)
63
+ intent.addFlags(Ti.Android.FLAG_GRANT_READ_URI_PERMISSION)
64
+ ```
65
+
66
+ `putExtraUri` makes the SDK convert the path into a `content://` URI served by `TiFileProvider`, which ships declared in the merged manifest — nothing to configure. **Without `FLAG_GRANT_READ_URI_PERMISSION` the receiving app cannot read the file.** It is granted a URI it has no permission for, so the share appears to work and the other app shows an error or an empty attachment.
67
+
68
+ Set `type` to the real MIME type rather than `*/*`. An unknown type falls back to `application/octet-stream`, which offers every app on the device instead of the ones that can do something useful.
69
+
70
+ ## 3. iOS: which module, and why one is still needed
71
+
72
+ There is no share sheet API in the SDK. This is verifiable rather than folklore — see [Verify instead of assuming](#9-verify-instead-of-assuming). The SDK offers `Ti.UI.iOS.DocumentViewer`, which is a previewer, and `Ti.App.iOS.UserActivity`, which is Handoff. Neither presents `UIActivityViewController`.
73
+
74
+ **The module is `dk.napp.social`**, and its provenance matters because the obvious repository is the wrong one:
75
+
76
+ | Repository | Status |
77
+ | --- | --- |
78
+ | [viezel/TiSocial.Framework](https://github.com/viezel/TiSocial.Framework) | **Archived April 2021.** Last release 2.0.0. This is the one search engines surface. |
79
+ | [hansemannn/TiSocial.Framework](https://github.com/hansemannn/TiSocial.Framework) | **The maintained fork.** Its 3.0.0 release states "Moved to this fork as the base repository". |
80
+
81
+ Releases on the fork: 3.0.0 (macOS support), 3.0.1 (fixes a possible iPad crash), 3.0.2 (restores iOS 12 compatibility). **3.0.2 is the highest published version.** If a project declares something higher in `tiapp.xml`, that build came from somewhere other than a release — treat the local copy as irreplaceable and say so in the project's install instructions, because nobody can download it.
82
+
83
+ ```javascript
84
+ function shareTextIos (text, subject) {
85
+ const social = require('dk.napp.social')
86
+
87
+ if (!social.isActivityViewSupported()) {
88
+ return false
89
+ }
90
+
91
+ social.activityView({ subject: subject || '', text }, [])
92
+ return true
93
+ }
94
+ ```
95
+
96
+ Require the module inside the iOS branch, not at the top of the file. A module-level `require` runs on Android too, where it resolves to nothing useful.
97
+
98
+ ## 4. Text is not a file
99
+
100
+ `DocumentViewer` takes the URL of a file; there is no way to hand it a string. So a file has two possible routes on iOS and a text has one.
101
+
102
+ **The file route via `DocumentViewer` works only when iOS can preview the type.** The viewer presents the document and its action button opens the real system sheet — no module involved. But "presents" is conditional: with a type the system cannot render, it presents nothing and reports no error. Silence, and the user sees a tap that did nothing.
103
+
104
+ So the decision is about the type, not about the file:
105
+
106
+ - **Type the system knows** (PDF, image, ZIP, plain text) → `DocumentViewer` is enough, and it saves you the module on this path.
107
+ - **Opaque type** — anything conforming to `public.data`, which is what a custom document type usually declares so that Files hands it over instead of previewing it (see [File type association](file-type-association.md)) → the viewer has nothing to show. Use the module's `activityView({ file })`.
108
+
109
+ This is the trap worth naming: **the same declaration that makes tapping the file open your app is what stops `DocumentViewer` from presenting it.** The two behaviors pull in opposite directions and both are correct.
110
+
111
+ ## 5. Do not pass text alongside a file
112
+
113
+ When sharing a file, pass only the file. Adding `text` to the same payload makes iOS treat the string as a **second item** in the sheet, and "Save to Files" writes two files: the document, plus a `.txt` holding the caption.
114
+
115
+ ```javascript
116
+ // wrong — "Save to Files" leaves the backup AND a stray text file
117
+ social.activityView({ file: file.nativePath, text: title }, [])
118
+
119
+ // right — the subject rides along without becoming an item
120
+ social.activityView({ file: file.nativePath, subject: title }, [])
121
+ ```
122
+
123
+ The symptom shows up only in destinations that write to disk. Sharing to a messaging app looks fine, which is why this survives testing.
124
+
125
+ ## 6. iPad needs an anchor
126
+
127
+ On iPad the sheet is a popover and must be anchored to the view that was tapped, or it has nowhere to attach:
128
+
129
+ ```javascript
130
+ if (Ti.Platform.osname === 'ipad') {
131
+ options.view = sourceView
132
+ social.activityPopover(options, [])
133
+ return true
134
+ }
135
+
136
+ social.activityView(options, [])
137
+ ```
138
+
139
+ This means the controller has to pass the source view down to the sharing service — usually `event.source` from the tap handler. Services that take only a URL cannot support iPad without changing their signature, which is why the parameter is worth adding before the app ships on tablets.
140
+
141
+ ## 7. Knowing whether the user actually shared
142
+
143
+ Both platforms open a sheet and return immediately. Neither the intent chooser nor a bare `activityView` call tells you what happened next.
144
+
145
+ On iOS the module emits it:
146
+
147
+ ```javascript
148
+ const social = require('dk.napp.social')
149
+
150
+ social.addEventListener('complete', event => {
151
+ // event.activityName — where it went
152
+ })
153
+
154
+ social.addEventListener('cancelled', () => {})
155
+ ```
156
+
157
+ Register these **once**, at module scope inside the iOS branch, not per share — re-adding on every tap leaks listeners.
158
+
159
+ Android has no equivalent through the chooser. If the product needs to know, that asymmetry is the design constraint, and treating "sheet opened" as success is the honest fallback.
160
+
161
+ Skip all of this when nothing depends on the answer. An analytics event or a "Shared!" confirmation is a reason; symmetry with iOS is not.
162
+
163
+ ## 8. Failing without an exception
164
+
165
+ Sharing fails for reasons that are not bugs: no activity, a device without sheet support, a file that was deleted between the tap and the call. **Return a boolean and let the caller decide whether to warn.** Throwing from a share function means an exception reaches whoever tapped a button.
166
+
167
+ ```javascript
168
+ function share (payload, sourceView) {
169
+ try {
170
+ return OS_ANDROID
171
+ ? shareAndroid(payload)
172
+ : shareIos(payload, sourceView)
173
+ } catch (error) {
174
+ Ti.API.error(`[sharing] Could not share: ${error.message}`)
175
+ return false
176
+ }
177
+ }
178
+ ```
179
+
180
+ The failure worth auditing in an existing codebase is a **mixed contract**: one branch returning `false` while its sibling throws or returns nothing. It usually means one path was hardened after a crash and the other was never revisited.
181
+
182
+ ## 9. Verify instead of assuming
183
+
184
+ Whether the SDK exposes something is checkable in the installed SDK's API metadata, and takes less time than the argument about it:
185
+
186
+ ```bash
187
+ JSCA=~/Library/Application\ Support/Titanium/mobilesdk/osx/<version>/api.jsca
188
+
189
+ python3 - "$JSCA" <<'PY'
190
+ import json, sys
191
+ d = json.load(open(sys.argv[1]))
192
+ for t in d.get('types', []):
193
+ if any(k in t['name'] for k in ('Activity', 'Share', 'Social', 'DocumentViewer')):
194
+ print(t['name'])
195
+ PY
196
+ ```
197
+
198
+ On 13.4.0.GA this returns `Titanium.UI.iOS.DocumentViewer`, `Titanium.App.iOS.UserActivity`, `Titanium.UI.ActivityIndicator` and the Android `Activity` types — and nothing wrapping `UIActivityViewController`. The same query tells you whether an API you are about to rely on is marked deprecated.
199
+
200
+ For the module, check what is actually installed rather than what `tiapp.xml` claims:
201
+
202
+ ```bash
203
+ ls ~/Library/Application\ Support/Titanium/modules/iphone/dk.napp.social/
204
+ cat ~/Library/Application\ Support/Titanium/modules/iphone/dk.napp.social/*/manifest
205
+ ```
206
+
207
+ **Do not verify a suspicion by comparing against a sibling project.** Two codebases that share a lineage share its mistakes, so agreement between them proves nothing. The control is the platform documentation or the SDK metadata.
208
+
209
+ ## 10. Symptom to cause
210
+
211
+ | Symptom | Cause |
212
+ | --- | --- |
213
+ | Tapping Share does nothing on iOS, no error | File type is not previewable and the code used `DocumentViewer`. Use the module for opaque types. |
214
+ | Receiving app shows an error or an empty attachment (Android) | Missing `FLAG_GRANT_READ_URI_PERMISSION`. |
215
+ | "Save to Files" writes two files | `text` passed alongside `file`. Use `subject`. |
216
+ | Sheet does not appear on iPad | `activityView` used instead of `activityPopover`, or no `view` anchor. |
217
+ | Crash when tapping Share on Android | `startActivity` on a null activity. Guard `rootActivity || currentActivity`. |
218
+ | Chooser offers every app on the device | `type` left as `*/*` or an unknown extension falling back to `application/octet-stream`. |
219
+ | `Couldn't find module: dk.napp.social` | Module not installed, or `require` at file scope running on Android. |
220
+ | Module version in `tiapp.xml` has no matching release | Locally built copy. It cannot be downloaded; document it as an install requirement. |
221
+
222
+ ## Sources
223
+
224
+ - Titanium SDK 13.4.0.GA `api.jsca` — the absence of a share sheet API on iOS and the non-deprecated status of `Ti.Android.createIntentChooser`.
225
+ - [hansemannn/TiSocial.Framework](https://github.com/hansemannn/TiSocial.Framework) — maintained fork, releases 3.0.0 through 3.0.2.
226
+ - [viezel/TiSocial.Framework](https://github.com/viezel/TiSocial.Framework) — original, archived April 2021.
227
+ - [ricardoalcocer/socialshare](https://github.com/ricardoalcocer/socialshare) — the historical cross-platform wrapper, for recognizing it in old code.
@@ -746,6 +746,7 @@ appStore.use(debugMiddleware)
746
746
  // controllers/debug/stateViewer.js (Development only)
747
747
  const { StateDebug } = require('services/stateDebug')
748
748
  const { appStore } = require('services/stateStore')
749
+ const { Navigation } = require('services/navigation')
749
750
 
750
751
  function init() {
751
752
  if (!Alloy.CFG.debug) {
@@ -779,12 +780,11 @@ function onHistoryItemClick(e) {
779
780
  const index = e.itemIndex
780
781
  const entry = StateDebug.getHistory()[index]
781
782
 
782
- // Show diff
783
- Ti.UI.createAlertDialog({
783
+ // Structured, potentially long content belongs in a scrollable debug screen.
784
+ Navigation.open('debug/stateDiff', {
784
785
  title: entry.action,
785
- message: JSON.stringify(entry.diff, null, 2),
786
- buttonNames: ['OK']
787
- }).show()
786
+ content: JSON.stringify(entry.diff, null, 2)
787
+ })
788
788
  }
789
789
 
790
790
  function cleanup() {