@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.
- package/README.md +15 -12
- package/package.json +1 -1
- package/skills/purgetss/SKILL.md +4 -9
- package/skills/ti-api/references/api-core.md +24 -69
- package/skills/ti-api/references/api-data-network.md +18 -63
- package/skills/ti-api/references/api-media.md +15 -49
- package/skills/ti-api/references/api-modules-coremotion-urlsession.md +6 -20
- package/skills/ti-api/references/api-modules-map.md +17 -40
- package/skills/ti-api/references/api-modules-nfc.md +23 -56
- package/skills/ti-api/references/api-ui-android.md +16 -37
- package/skills/ti-api/references/api-ui-extras.md +28 -72
- package/skills/ti-api/references/api-ui-lists.md +14 -35
- package/skills/ti-api/references/api-ui-text-input.md +13 -39
- package/skills/ti-api/references/api-ui-windows-navigation.md +17 -43
- package/skills/ti-api/references/api-xml-global.md +25 -60
- package/skills/ti-expert/SKILL.md +35 -5
- package/skills/ti-expert/references/adaptive-layouts.md +15 -43
- package/skills/ti-expert/references/alloy-builtins.md +8 -22
- package/skills/ti-expert/references/alloy-structure.md +34 -18
- package/skills/ti-expert/references/anti-patterns.md +18 -39
- package/skills/ti-expert/references/code-conventions.md +5 -3
- package/skills/ti-expert/references/controller-patterns.md +31 -26
- package/skills/ti-expert/references/examples.md +41 -21
- package/skills/ti-expert/references/feedback-migration.md +143 -0
- package/skills/ti-expert/references/feedback-surfaces.md +111 -0
- package/skills/ti-expert/references/feedback-widget-contracts.md +175 -0
- package/skills/ti-expert/references/file-type-association.md +265 -0
- package/skills/ti-expert/references/migration-patterns.md +37 -25
- package/skills/ti-expert/references/security-device.md +58 -56
- package/skills/ti-expert/references/sharing.md +227 -0
- package/skills/ti-expert/references/state-management.md +5 -5
- package/skills/ti-expert/references/theming.md +109 -392
- package/skills/ti-guides/references/coding-best-practices.md +1 -2
- package/skills/ti-guides/references/style-and-conventions.md +1 -2
- package/skills/ti-howtos/references/debugging-profiling.md +1 -2
- package/skills/ti-howtos/references/location-and-maps.md +1 -2
- package/skills/ti-howtos/references/using-modules.md +1 -2
- package/skills/ti-howtos/references/web-content-integration.md +1 -2
- package/skills/ti-howtos/references/webpack-build-pipeline.md +1 -2
- package/skills/ti-ui/references/listviews-and-performance.md +5 -10
- 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
|
-
//
|
|
147
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
507
|
-
return true
|
|
511
|
+
return { outcome: 'warn', reasons, restricted: true }
|
|
508
512
|
|
|
509
513
|
case 'warn':
|
|
510
514
|
default:
|
|
511
|
-
|
|
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
|
-
//
|
|
554
|
-
const
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
560
|
+
function cleanup() {
|
|
561
|
+
$.dialog.destroy()
|
|
562
|
+
$.destroy()
|
|
559
563
|
}
|
|
560
564
|
|
|
561
|
-
|
|
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
|
-
//
|
|
783
|
-
|
|
783
|
+
// Structured, potentially long content belongs in a scrollable debug screen.
|
|
784
|
+
Navigation.open('debug/stateDiff', {
|
|
784
785
|
title: entry.action,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}).show()
|
|
786
|
+
content: JSON.stringify(entry.diff, null, 2)
|
|
787
|
+
})
|
|
788
788
|
}
|
|
789
789
|
|
|
790
790
|
function cleanup() {
|