@maccesar/titools 2.2.12 → 2.3.0
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 +255 -713
- package/bin/titools.js +1 -1
- package/lib/commands/update.js +5 -6
- package/lib/config.js +1 -0
- package/lib/downloader.js +10 -0
- package/package.json +1 -1
- package/skills/alloy-guides/SKILL.md +46 -48
- package/skills/alloy-guides/references/CONTROLLERS.md +4 -6
- package/skills/alloy-guides/references/MODELS.md +340 -184
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +3 -4
- package/skills/alloy-guides/references/VIEWS_STYLES.md +44 -46
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +4 -5
- package/skills/alloy-guides/references/VIEWS_XML.md +51 -17
- package/skills/alloy-guides/references/WIDGETS.md +1 -1
- package/skills/alloy-howtos/SKILL.md +12 -13
- package/skills/alloy-howtos/references/cli_reference.md +40 -41
- package/skills/alloy-howtos/references/samples.md +3 -4
- package/skills/purgetss/SKILL.md +369 -356
- package/skills/purgetss/references/EXAMPLES.md +24 -25
- package/skills/purgetss/references/animation-advanced.md +555 -0
- package/skills/purgetss/references/animation-system.md +395 -995
- package/skills/purgetss/references/apply-directive.md +111 -141
- package/skills/purgetss/references/arbitrary-values.md +206 -480
- package/skills/purgetss/references/class-categories.md +277 -0
- package/skills/purgetss/references/class-index.md +1 -420
- package/skills/purgetss/references/cli-commands.md +446 -556
- package/skills/purgetss/references/configurable-properties.md +163 -599
- package/skills/purgetss/references/custom-rules.md +33 -76
- package/skills/purgetss/references/customization-deep-dive.md +319 -518
- package/skills/purgetss/references/dynamic-component-creation.md +33 -37
- package/skills/purgetss/references/grid-layout.md +42 -371
- package/skills/purgetss/references/icon-fonts.md +82 -475
- package/skills/purgetss/references/installation-setup.md +159 -331
- package/skills/purgetss/references/migration-guide.md +54 -109
- package/skills/purgetss/references/opacity-modifier.md +25 -222
- package/skills/purgetss/references/performance-tips.md +2 -2
- package/skills/purgetss/references/platform-modifiers.md +21 -407
- package/skills/purgetss/references/tikit-components.md +237 -104
- package/skills/purgetss/references/titanium-resets.md +20 -21
- package/skills/purgetss/references/ui-ux-design.md +171 -1702
- package/skills/ti-api/SKILL.md +109 -0
- package/skills/ti-api/references/api-android.md +675 -0
- package/skills/ti-api/references/api-app-platform.md +636 -0
- package/skills/ti-api/references/api-core.md +764 -0
- package/skills/ti-api/references/api-data-network.md +641 -0
- package/skills/ti-api/references/api-media.md +655 -0
- package/skills/ti-api/references/api-modules-ble-bluetooth.md +657 -0
- package/skills/ti-api/references/api-modules-coremotion-urlsession.md +411 -0
- package/skills/ti-api/references/api-modules-map.md +632 -0
- package/skills/ti-api/references/api-modules-nfc.md +725 -0
- package/skills/ti-api/references/api-modules-social-misc.md +526 -0
- package/skills/ti-api/references/api-services.md +700 -0
- package/skills/ti-api/references/api-ui-android.md +499 -0
- package/skills/ti-api/references/api-ui-extras.md +702 -0
- package/skills/ti-api/references/api-ui-ios-animator.md +378 -0
- package/skills/ti-api/references/api-ui-ios.md +756 -0
- package/skills/ti-api/references/api-ui-lists.md +581 -0
- package/skills/ti-api/references/api-ui-text-input.md +607 -0
- package/skills/ti-api/references/api-ui-views.md +572 -0
- package/skills/ti-api/references/api-ui-windows-navigation.md +676 -0
- package/skills/ti-api/references/api-xml-global.md +743 -0
- package/skills/ti-expert/SKILL.md +46 -45
- package/skills/ti-expert/references/adaptive-layouts.md +414 -0
- package/skills/ti-expert/references/alloy-builtins.md +16 -20
- package/skills/ti-expert/references/alloy-structure.md +40 -42
- package/skills/ti-expert/references/anti-patterns.md +34 -0
- package/skills/ti-expert/references/code-conventions.md +5 -3
- package/skills/ti-expert/references/error-handling.md +14 -7
- package/skills/ti-expert/references/examples.md +4 -2
- package/skills/ti-expert/references/performance-optimization.md +26 -24
- package/skills/ti-expert/references/security-device.md +17 -14
- package/skills/ti-expert/references/security-fundamentals.md +60 -101
- package/skills/ti-expert/references/state-management.md +15 -14
- package/skills/ti-expert/references/testing-e2e-ci.md +30 -21
- package/skills/ti-expert/references/theming.md +12 -20
- package/skills/ti-guides/SKILL.md +13 -17
- package/skills/ti-guides/references/advanced-data-and-images.md +30 -10
- package/skills/ti-guides/references/application-frameworks.md +3 -3
- package/skills/ti-guides/references/coding-best-practices.md +31 -2
- package/skills/ti-guides/references/commonjs-advanced.md +46 -4
- package/skills/ti-guides/references/hello-world.md +9 -13
- package/skills/ti-guides/references/hyperloop-native-access.md +4 -1
- package/skills/ti-guides/references/javascript-primer.md +13 -5
- package/skills/ti-guides/references/resources.md +0 -2
- package/skills/ti-guides/references/style-and-conventions.md +1 -0
- package/skills/ti-guides/references/tiapp-config.md +0 -32
- package/skills/ti-howtos/SKILL.md +43 -45
- package/skills/ti-howtos/references/buffer-codec-streams.md +25 -3
- package/skills/ti-howtos/references/debugging-profiling.md +14 -25
- package/skills/ti-howtos/references/google-maps-v2.md +3 -3
- package/skills/ti-howtos/references/ios-map-kit.md +8 -1
- package/skills/ti-howtos/references/notification-services.md +0 -1
- package/skills/ti-howtos/references/webpack-build-pipeline.md +3 -0
- package/skills/ti-ui/SKILL.md +47 -49
- package/skills/ti-ui/references/application-structures.md +3 -2
- package/skills/ti-ui/references/orientation.md +17 -9
- package/skills/ti-ui/references/platform-ui-ios.md +63 -0
- package/skills/ti-ui/references/scrolling-views.md +39 -0
|
@@ -66,45 +66,44 @@ app/
|
|
|
66
66
|
|
|
67
67
|
## lib/ folder and module require paths
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
When Alloy compiles, the **entire `lib/` folder is flattened to the root of Resources**. This means:
|
|
71
|
-
- `app/lib/services/authService.js` → `Resources/iphone/services/authService.js`
|
|
72
|
-
- `app/lib/api/authApi.js` → `Resources/iphone/api/authApi.js`
|
|
73
|
-
|
|
74
|
-
**Therefore, require statements should NOT include `lib/` prefix:**
|
|
75
|
-
```javascript
|
|
76
|
-
// ❌ WRONG - Will fail at runtime
|
|
77
|
-
const authApi = require('lib/api/authApi')
|
|
78
|
-
|
|
79
|
-
// ✅ CORRECT - Path relative to flattened lib/
|
|
80
|
-
const authApi = require('api/authApi')
|
|
81
|
-
const authService = require('services/authService')
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**This applies to:**
|
|
85
|
-
- All files in `app/lib/` (services, api, helpers, etc.)
|
|
86
|
-
- Cross-references within lib/ files
|
|
87
|
-
- Controller requires of lib/ files
|
|
88
|
-
|
|
89
|
-
**Example project structure:**
|
|
90
|
-
```
|
|
91
|
-
app/
|
|
92
|
-
├── lib/
|
|
93
|
-
│ ├── services/
|
|
94
|
-
│ │ ├── authService.js # require('services/navigationService')
|
|
95
|
-
│ │ ├── navigationService.js # require('services/notificationService')
|
|
96
|
-
│ │ └── notificationService.js
|
|
97
|
-
│ ├── api/
|
|
98
|
-
│ │ ├── authApi.js # require('services/authService')
|
|
99
|
-
│ │ ├── userApi.js
|
|
100
|
-
│ │ └── frameApi.js
|
|
101
|
-
│ └── repositories/
|
|
102
|
-
│ ├── userRepository.js
|
|
103
|
-
│ └── settingsRepository.js
|
|
104
|
-
├── controllers/
|
|
105
|
-
│ └── index.js # require('services/authService')
|
|
106
|
-
```
|
|
107
|
-
:::
|
|
69
|
+
> **🚨 CRITICAL: lib/ Folder is FLATTENED During Build**
|
|
70
|
+
> When Alloy compiles, the **entire `lib/` folder is flattened to the root of Resources**. This means:
|
|
71
|
+
> - `app/lib/services/authService.js` → `Resources/iphone/services/authService.js`
|
|
72
|
+
> - `app/lib/api/authApi.js` → `Resources/iphone/api/authApi.js`
|
|
73
|
+
>
|
|
74
|
+
> **Therefore, require statements should NOT include `lib/` prefix:**
|
|
75
|
+
> ```javascript
|
|
76
|
+
> // ❌ WRONG - Will fail at runtime
|
|
77
|
+
> const authApi = require('lib/api/authApi')
|
|
78
|
+
>
|
|
79
|
+
> // ✅ CORRECT - Path relative to flattened lib/
|
|
80
|
+
> const authApi = require('api/authApi')
|
|
81
|
+
> const authService = require('services/authService')
|
|
82
|
+
> ```
|
|
83
|
+
>
|
|
84
|
+
> **This applies to:**
|
|
85
|
+
> - All files in `app/lib/` (services, api, helpers, etc.)
|
|
86
|
+
> - Cross-references within lib/ files
|
|
87
|
+
> - Controller requires of lib/ files
|
|
88
|
+
>
|
|
89
|
+
> **Example project structure:**
|
|
90
|
+
> ```
|
|
91
|
+
> app/
|
|
92
|
+
> ├── lib/
|
|
93
|
+
> │ ├── services/
|
|
94
|
+
> │ │ ├── authService.js # require('services/navigationService')
|
|
95
|
+
> │ │ ├── navigationService.js # require('services/notificationService')
|
|
96
|
+
> │ │ └── notificationService.js
|
|
97
|
+
> │ ├── api/
|
|
98
|
+
> │ │ ├── authApi.js # require('services/authService')
|
|
99
|
+
> │ │ ├── userApi.js
|
|
100
|
+
> │ │ └── frameApi.js
|
|
101
|
+
> │ └── repositories/
|
|
102
|
+
> │ ├── userRepository.js
|
|
103
|
+
> │ └── settingsRepository.js
|
|
104
|
+
> ├── controllers/
|
|
105
|
+
> │ └── index.js # require('services/authService')
|
|
106
|
+
> ```
|
|
108
107
|
|
|
109
108
|
## Data layer: two approaches
|
|
110
109
|
|
|
@@ -338,9 +337,8 @@ const loadData = () => {
|
|
|
338
337
|
}
|
|
339
338
|
```
|
|
340
339
|
|
|
341
|
-
|
|
342
|
-
Widgets have their own `styles/widget.tss` file. Define all widget-specific styles there to keep them self-contained and portable.
|
|
343
|
-
:::
|
|
340
|
+
> **💡 Widget Styles**
|
|
341
|
+
> Widgets have their own `styles/widget.tss` file. Define all widget-specific styles there to keep them self-contained and portable.
|
|
344
342
|
|
|
345
343
|
### Widget ↔ controller communication
|
|
346
344
|
|
|
@@ -76,6 +76,39 @@
|
|
|
76
76
|
- iOS: Use `Ti.UI.iOS.createDocumentViewer` for files, or simple `Ti.UI.createOptionDialog` + `Ti.UI.Clipboard` for links
|
|
77
77
|
- Android: Use `Ti.Android.createIntent` with ACTION_SEND
|
|
78
78
|
|
|
79
|
+
## Community-Discovered Patterns
|
|
80
|
+
|
|
81
|
+
### 15. Using extendEdges without autoAdjustScrollViewInsets (iOS)
|
|
82
|
+
|
|
83
|
+
**Anti-pattern:** Setting `extendEdges: [Ti.UI.EXTEND_EDGE_ALL]` on a Window without also setting `autoAdjustScrollViewInsets: true`.
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// Wrong: content overlaps behind navigation bar
|
|
87
|
+
const win = Ti.UI.createWindow({
|
|
88
|
+
title: 'My Screen',
|
|
89
|
+
largeTitleEnabled: true,
|
|
90
|
+
extendEdges: [Ti.UI.EXTEND_EDGE_ALL]
|
|
91
|
+
// missing autoAdjustScrollViewInsets!
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Why it's wrong:** `extendEdges` tells iOS to extend the view's content behind the navigation bar and tab bar. Without `autoAdjustScrollViewInsets: true`, iOS does not adjust the ScrollView's content insets, so the scroll content starts at y=0 — directly behind the navigation bar.
|
|
96
|
+
|
|
97
|
+
**Fix:** Always pair `extendEdges` with `autoAdjustScrollViewInsets`:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const win = Ti.UI.createWindow({
|
|
101
|
+
title: 'My Screen',
|
|
102
|
+
largeTitleEnabled: true,
|
|
103
|
+
extendEdges: [Ti.UI.EXTEND_EDGE_ALL],
|
|
104
|
+
autoAdjustScrollViewInsets: true
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
These three properties work together: `extendEdges` creates the blur/translucent effect, `autoAdjustScrollViewInsets` prevents content overlap, and `largeTitleEnabled` shows the collapsible large title. Without `extendEdges`, the large title also renders with a visible delay (empty nav bar gap appears first, then the title draws).
|
|
109
|
+
|
|
110
|
+
This applies to Windows inside both standalone NavigationWindow and TabGroup (which wraps each Tab in an implicit NavigationWindow on iOS).
|
|
111
|
+
|
|
79
112
|
---
|
|
80
113
|
|
|
81
114
|
## Quick reference table
|
|
@@ -88,3 +121,4 @@
|
|
|
88
121
|
| `lib/` prefix | lib/ is flattened | Use path without lib/ |
|
|
89
122
|
| `$.index.open()` | Wrong ID reference | Use actual Window ID |
|
|
90
123
|
| `createNotification` | API doesn't exist | `createAlertDialog` |
|
|
124
|
+
| `extendEdges` alone | Content behind nav bar | Add `autoAdjustScrollViewInsets: true` |
|
|
@@ -386,13 +386,13 @@ const Backbone = require('alloy/backbone')
|
|
|
386
386
|
|
|
387
387
|
const EventBus = _.clone(Backbone.Events)
|
|
388
388
|
|
|
389
|
-
//
|
|
390
|
-
|
|
389
|
+
// Attach event constants directly to the bus object so they survive module.exports
|
|
390
|
+
EventBus.Events = {
|
|
391
391
|
USER_UPDATED: 'user:updated',
|
|
392
392
|
SYNC_COMPLETE: 'sync:complete'
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
//
|
|
395
|
+
// Export the bus (EventBus.Events is accessible as a property)
|
|
396
396
|
module.exports = EventBus
|
|
397
397
|
|
|
398
398
|
// Usage
|
|
@@ -400,6 +400,8 @@ const EventBus = require('services/eventBus')
|
|
|
400
400
|
const { Events } = EventBus
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
+
> Note: setting `exports.Events = {...}` before `module.exports = EventBus` has no effect — the `module.exports` assignment discards the prior `exports` object. Attach constants directly to the object being exported instead.
|
|
404
|
+
|
|
403
405
|
### Re-exports (Barrel Files)
|
|
404
406
|
```javascript
|
|
405
407
|
// lib/api/index.js - Barrel file
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
```javascript
|
|
6
6
|
// lib/core/appError.js
|
|
7
|
-
module.exports = class
|
|
7
|
+
// IMPORTANT: Use named exports (exports.Foo), NOT module.exports = class Foo.
|
|
8
|
+
// Multiple `module.exports =` assignments overwrite each other — only the last one survives.
|
|
9
|
+
|
|
10
|
+
class AppError extends Error {
|
|
8
11
|
constructor(message, code, statusCode = 500) {
|
|
9
12
|
super(message)
|
|
10
13
|
this.code = code
|
|
@@ -14,32 +17,36 @@ module.exports = class AppError extends Error {
|
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
module.exports = class NetworkError extends AppError {
|
|
20
|
+
class NetworkError extends AppError {
|
|
19
21
|
constructor(message = 'Network request failed') {
|
|
20
22
|
super(message, 'NETWORK_ERROR', 0)
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
class AuthError extends AppError {
|
|
25
27
|
constructor(message = 'Authentication failed') {
|
|
26
28
|
super(message, 'AUTH_ERROR', 401)
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
class ValidationError extends AppError {
|
|
31
33
|
constructor(message = 'Validation failed') {
|
|
32
34
|
super(message, 'VALIDATION_ERROR', 400)
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
class NotFoundError extends AppError {
|
|
37
39
|
constructor(message = 'Resource not found') {
|
|
38
40
|
super(message, 'NOT_FOUND', 404)
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
exports.AppError = AppError
|
|
45
|
+
exports.NetworkError = NetworkError
|
|
46
|
+
exports.AuthError = AuthError
|
|
47
|
+
exports.ValidationError = ValidationError
|
|
48
|
+
exports.NotFoundError = NotFoundError
|
|
49
|
+
|
|
43
50
|
exports.ErrorCodes = {
|
|
44
51
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
45
52
|
AUTH_ERROR: 'AUTH_ERROR',
|
|
@@ -736,9 +736,11 @@ function init() {
|
|
|
736
736
|
}
|
|
737
737
|
|
|
738
738
|
function onTabFocus(e) {
|
|
739
|
-
//
|
|
739
|
+
// Track tab changes via your analytics service
|
|
740
|
+
// Note: Ti.Analytics was removed from the Titanium SDK.
|
|
741
|
+
// Use a third-party analytics service (e.g., Firebase, Segment) instead.
|
|
740
742
|
const tabNames = ['home', 'search', 'cart', 'profile']
|
|
741
|
-
|
|
743
|
+
Analytics.track(`tab:${tabNames[e.index]}`)
|
|
742
744
|
}
|
|
743
745
|
|
|
744
746
|
function onCartUpdated({ itemCount }) {
|
|
@@ -212,12 +212,14 @@ $.cleanup = cleanup
|
|
|
212
212
|
```javascript
|
|
213
213
|
// lib/services/database.js
|
|
214
214
|
exports.DB = {
|
|
215
|
-
// Use transactions
|
|
215
|
+
// Use transactions to speed batch inserts
|
|
216
|
+
// Per official docs: use db.execute('BEGIN') / db.execute('COMMIT') — there are no
|
|
217
|
+
// dedicated begin_transaction() / commit() / rollback() methods on Ti.Database.DB.
|
|
216
218
|
batchInsert(items) {
|
|
217
219
|
const db = Ti.Database.open('mydb')
|
|
218
220
|
|
|
219
221
|
try {
|
|
220
|
-
db.
|
|
222
|
+
db.execute('BEGIN')
|
|
221
223
|
|
|
222
224
|
items.forEach(item => {
|
|
223
225
|
db.execute(
|
|
@@ -228,12 +230,12 @@ exports.DB = {
|
|
|
228
230
|
)
|
|
229
231
|
})
|
|
230
232
|
|
|
231
|
-
db.
|
|
233
|
+
db.execute('COMMIT')
|
|
232
234
|
|
|
233
235
|
console.log(`Inserted ${items.length} items`)
|
|
234
236
|
|
|
235
237
|
} catch (e) {
|
|
236
|
-
db.
|
|
238
|
+
db.execute('ROLLBACK')
|
|
237
239
|
console.error('Batch insert failed', e)
|
|
238
240
|
throw e
|
|
239
241
|
|
|
@@ -574,14 +576,14 @@ function cleanup() {
|
|
|
574
576
|
|
|
575
577
|
### Common use cases
|
|
576
578
|
|
|
577
|
-
| Pattern
|
|
579
|
+
| Pattern | Use Case | Delay |
|
|
578
580
|
| -------- | ---------------- | ------------ |
|
|
579
|
-
| Debounce | Search input
|
|
580
|
-
| Debounce | Auto-save
|
|
581
|
-
| Debounce | Window resize
|
|
582
|
-
| Throttle | Scroll events
|
|
581
|
+
| Debounce | Search input | 300ms |
|
|
582
|
+
| Debounce | Auto-save | 1000ms |
|
|
583
|
+
| Debounce | Window resize | 150ms |
|
|
584
|
+
| Throttle | Scroll events | 50-100ms |
|
|
583
585
|
| Throttle | Mouse/touch move | 16ms (60fps) |
|
|
584
|
-
| Throttle | API polling
|
|
586
|
+
| Throttle | API polling | 5000ms+ |
|
|
585
587
|
|
|
586
588
|
### Combined pattern for real-time + final
|
|
587
589
|
|
|
@@ -622,18 +624,18 @@ function cleanup() {
|
|
|
622
624
|
|
|
623
625
|
## Performance checklist
|
|
624
626
|
|
|
625
|
-
| Area
|
|
627
|
+
| Area | Check |
|
|
626
628
|
| ------------- | --------------------------------------- |
|
|
627
|
-
| **Bridge**
|
|
628
|
-
| **Bridge**
|
|
629
|
-
| **Bridge**
|
|
630
|
-
| **Memory**
|
|
631
|
-
| **Memory**
|
|
632
|
-
| **Memory**
|
|
633
|
-
| **Database**
|
|
634
|
-
| **Database**
|
|
635
|
-
| **Database**
|
|
636
|
-
| **Animation** | Using native animations, not intervals
|
|
637
|
-
| **Animation** | GPU-accelerated properties preferred
|
|
638
|
-
| **Timing**
|
|
639
|
-
| **Timing**
|
|
629
|
+
| **Bridge** | Cached Ti.Platform properties |
|
|
630
|
+
| **Bridge** | Using applyProperties for updates |
|
|
631
|
+
| **Bridge** | TSS styles instead of inline attributes |
|
|
632
|
+
| **Memory** | All global listeners cleaned up |
|
|
633
|
+
| **Memory** | Heavy objects nulled in cleanup |
|
|
634
|
+
| **Memory** | Images resized appropriately |
|
|
635
|
+
| **Database** | Using transactions for batch ops |
|
|
636
|
+
| **Database** | Indexes on frequently queried columns |
|
|
637
|
+
| **Database** | ResultSets and DB handles closed |
|
|
638
|
+
| **Animation** | Using native animations, not intervals |
|
|
639
|
+
| **Animation** | GPU-accelerated properties preferred |
|
|
640
|
+
| **Timing** | Debounce on search/input |
|
|
641
|
+
| **Timing** | Throttle on scroll/touch events |
|
|
@@ -502,6 +502,9 @@ exports.SecurityService = {
|
|
|
502
502
|
},
|
|
503
503
|
|
|
504
504
|
_showBlockedScreen() {
|
|
505
|
+
// Note: iOS guidelines prohibit programmatically terminating an app.
|
|
506
|
+
// The best practice is to show a blocking dialog and prevent further interaction.
|
|
507
|
+
// On Android you can call Ti.Android.currentActivity.finish() to close the activity.
|
|
505
508
|
const dialog = Ti.UI.createAlertDialog({
|
|
506
509
|
title: L('security_blocked_title'),
|
|
507
510
|
message: L('security_blocked_msg'),
|
|
@@ -509,10 +512,10 @@ exports.SecurityService = {
|
|
|
509
512
|
})
|
|
510
513
|
|
|
511
514
|
dialog.addEventListener('click', () => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
Ti.Platform.openURL('prefs:root=General')
|
|
515
|
+
if (OS_ANDROID) {
|
|
516
|
+
Ti.Android.currentActivity.finish()
|
|
515
517
|
}
|
|
518
|
+
// On iOS: leave the dialog visible — Apple guidelines prohibit force-quitting apps.
|
|
516
519
|
})
|
|
517
520
|
|
|
518
521
|
dialog.show()
|
|
@@ -550,15 +553,15 @@ setInterval(() => {
|
|
|
550
553
|
|
|
551
554
|
## Security checklist
|
|
552
555
|
|
|
553
|
-
| Category
|
|
556
|
+
| Category | Check | Implementation |
|
|
554
557
|
| -------------- | -------------------------- | ------------------------ |
|
|
555
|
-
| **Biometrics** | Use ti.identity for auth
|
|
556
|
-
| **Biometrics** | Never store biometric data | System handles storage
|
|
557
|
-
| **Biometrics** | Fallback to password
|
|
558
|
-
| **Deep Links** | Whitelist allowed schemes
|
|
559
|
-
| **Deep Links** | Whitelist allowed hosts
|
|
560
|
-
| **Deep Links** | Sanitize all parameters
|
|
561
|
-
| **Deep Links** | Check auth requirements
|
|
562
|
-
| **Integrity**
|
|
563
|
-
| **Integrity**
|
|
564
|
-
| **Integrity**
|
|
558
|
+
| **Biometrics** | Use ti.identity for auth | BiometricService wrapper |
|
|
559
|
+
| **Biometrics** | Never store biometric data | System handles storage |
|
|
560
|
+
| **Biometrics** | Fallback to password | Always offer alternative |
|
|
561
|
+
| **Deep Links** | Whitelist allowed schemes | ALLOWED_SCHEMES constant |
|
|
562
|
+
| **Deep Links** | Whitelist allowed hosts | ALLOWED_HOSTS constant |
|
|
563
|
+
| **Deep Links** | Sanitize all parameters | _sanitizeParams() |
|
|
564
|
+
| **Deep Links** | Check auth requirements | requiresAuth per route |
|
|
565
|
+
| **Integrity** | Check for jailbreak/root | checkDeviceIntegrity() |
|
|
566
|
+
| **Integrity** | Define security policy | block/restrict/warn |
|
|
567
|
+
| **Integrity** | Log security events | Always log compromises |
|
|
@@ -4,107 +4,90 @@
|
|
|
4
4
|
|
|
5
5
|
**NEVER store tokens in:** `Ti.App.Properties` (plaintext), localStorage, or files.
|
|
6
6
|
|
|
7
|
-
**USE
|
|
7
|
+
**USE the `ti.identity` module** — it handles iOS Keychain and Android Keystore through a unified API. Both platforms use `Identity.createKeychainItem()`.
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
10
|
// lib/services/tokenStorage.js
|
|
11
|
+
const Identity = require('ti.identity')
|
|
12
|
+
|
|
13
|
+
// Create a keychainItem once per identifier
|
|
14
|
+
function createItem(identifier) {
|
|
15
|
+
return Identity.createKeychainItem({ identifier })
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
exports.TokenStorage = {
|
|
12
19
|
save(token) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
})
|
|
18
|
-
keyStore.addEntry('authToken', token)
|
|
19
|
-
} else {
|
|
20
|
-
// Use iOS Keychain
|
|
21
|
-
Ti.KeychainItem.setItem({
|
|
22
|
-
identifier: 'authToken',
|
|
23
|
-
value: token,
|
|
24
|
-
accessGroup: 'com.yourapp.keychain'
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const item = createItem('authToken')
|
|
22
|
+
item.addEventListener('save', (e) => {
|
|
23
|
+
e.success ? resolve() : reject(new Error(e.error))
|
|
25
24
|
})
|
|
26
|
-
|
|
25
|
+
item.save(token)
|
|
26
|
+
})
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
get() {
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const item = createItem('authToken')
|
|
32
|
+
item.addEventListener('read', (e) => {
|
|
33
|
+
e.success ? resolve(e.value) : reject(new Error(e.error))
|
|
33
34
|
})
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
return Ti.KeychainItem.getItem({
|
|
37
|
-
identifier: 'authToken',
|
|
38
|
-
accessGroup: 'com.yourapp.keychain'
|
|
39
|
-
})
|
|
40
|
-
}
|
|
35
|
+
item.read()
|
|
36
|
+
})
|
|
41
37
|
},
|
|
42
38
|
|
|
43
39
|
clear() {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
keyStore.removeEntry('authToken')
|
|
49
|
-
} else {
|
|
50
|
-
Ti.KeychainItem.removeItem({
|
|
51
|
-
identifier: 'authToken',
|
|
52
|
-
accessGroup: 'com.yourapp.keychain'
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const item = createItem('authToken')
|
|
42
|
+
item.addEventListener('reset', (e) => {
|
|
43
|
+
e.success ? resolve() : reject(new Error(e.error))
|
|
53
44
|
})
|
|
54
|
-
|
|
45
|
+
item.reset()
|
|
46
|
+
})
|
|
55
47
|
}
|
|
56
48
|
}
|
|
57
49
|
```
|
|
58
50
|
|
|
51
|
+
> Verified against official `ti.identity` module docs. `Identity.createKeychainItem({identifier})` is the cross-platform API — it maps to iOS Keychain and Android Keystore automatically. `Ti.Android.createKeyStore()` and `Ti.KeychainItem.setItem()` do NOT exist.
|
|
52
|
+
|
|
59
53
|
## Certificate pinning
|
|
60
54
|
|
|
61
|
-
Prevent man-in-the-middle attacks by pinning SSL certificates:
|
|
55
|
+
Prevent man-in-the-middle attacks by pinning SSL certificates using the **`ti.https` module** (community module — not built into the SDK):
|
|
62
56
|
|
|
63
57
|
```javascript
|
|
64
58
|
// lib/api/pinnedClient.js
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
// Requires: ti.https module — install via npm or tiapp.xml modules section
|
|
60
|
+
const HTTPS = require('ti.https')
|
|
61
|
+
|
|
62
|
+
const securityManager = HTTPS.createX509CertificatePinningSecurityManager([
|
|
63
|
+
{
|
|
64
|
+
url: 'https://api.example.com',
|
|
65
|
+
serverCertificate: Ti.Filesystem.getFile(
|
|
66
|
+
Ti.Filesystem.resourcesDirectory, 'certificates/api-pin.pem'
|
|
67
|
+
).read()
|
|
68
|
+
}
|
|
69
|
+
])
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
exports.createPinnedClient = function(options = {}) {
|
|
72
|
+
return Ti.Network.createHTTPClient({
|
|
73
|
+
securityManager, // must be set at creation time
|
|
71
74
|
validatesSecureCertificate: true,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Success
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
onerror: (e) => {
|
|
78
|
-
// Certificate validation failed
|
|
79
|
-
if (e.error.indexOf('certificate') >= 0) {
|
|
80
|
-
Ti.API.error('Certificate pinning failed - possible MITM attack')
|
|
81
|
-
}
|
|
82
|
-
}
|
|
75
|
+
timeout: 10000,
|
|
76
|
+
...options
|
|
83
77
|
})
|
|
84
|
-
|
|
85
|
-
return client
|
|
86
78
|
}
|
|
87
79
|
```
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```xml
|
|
92
|
-
<ti:app>
|
|
93
|
-
<certificates>
|
|
94
|
-
<certificate>
|
|
95
|
-
<name>api.example.com</name>
|
|
96
|
-
<type>rsa</type>
|
|
97
|
-
<file>certificates/api-pin.pem</file>
|
|
98
|
-
</certificate>
|
|
99
|
-
</certificates>
|
|
100
|
-
</ti:app>
|
|
101
|
-
```
|
|
81
|
+
> `certificatePinning: true` is NOT a valid `HTTPClient` property. Certificate pinning requires the `ti.https` module and must be set via the `securityManager` property when creating the client. Note that the `validatesSecureCertificate` property of `HTTPClient` is not honored for pinned URLs — the security manager takes precedence.
|
|
102
82
|
|
|
103
83
|
## Data encryption at rest
|
|
104
84
|
|
|
85
|
+
> **Community pattern** — `ti.crypto` is a third-party community module (not part of the Titanium SDK core). Verify availability and compatibility before using in production.
|
|
86
|
+
|
|
105
87
|
```javascript
|
|
106
88
|
// lib/services/encryption.js
|
|
107
89
|
// AES-256 encryption for sensitive local data
|
|
90
|
+
// Requires: ti.crypto community module
|
|
108
91
|
|
|
109
92
|
const crypto = require('ti.crypto')
|
|
110
93
|
|
|
@@ -125,30 +108,6 @@ exports.decrypt = function(encryptedData, key) {
|
|
|
125
108
|
options: { mode: crypto.CBC }
|
|
126
109
|
})
|
|
127
110
|
}
|
|
128
|
-
|
|
129
|
-
// Usage: Secure cache of sensitive user data
|
|
130
|
-
module.exports = class SecureCache {
|
|
131
|
-
constructor(encryptionKey) {
|
|
132
|
-
this.key = encryptionKey
|
|
133
|
-
this.cache = {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
set(key, value) {
|
|
137
|
-
const encrypted = encrypt(JSON.stringify(value), this.key)
|
|
138
|
-
this.cache[key] = encrypted
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
get(key) {
|
|
142
|
-
if (!this.cache[key]) return null
|
|
143
|
-
|
|
144
|
-
const decrypted = decrypt(this.cache[key], this.key)
|
|
145
|
-
return JSON.parse(decrypted)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
clear() {
|
|
149
|
-
this.cache = {}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
111
|
```
|
|
153
112
|
|
|
154
113
|
## Secure HTTP communication
|
|
@@ -270,15 +229,15 @@ exports.Validator = {
|
|
|
270
229
|
|
|
271
230
|
## Owasp mobile security checklist
|
|
272
231
|
|
|
273
|
-
| Category
|
|
232
|
+
| Category | Check | Implementation |
|
|
274
233
|
| -------------------- | --------------------------- | ---------------------------------- |
|
|
275
|
-
| **Data Storage**
|
|
276
|
-
| **Data Storage**
|
|
277
|
-
| **Communication**
|
|
278
|
-
| **Communication**
|
|
279
|
-
| **Authentication**
|
|
280
|
-
| **Authentication**
|
|
281
|
-
| **Input Validation** | Server-side validation
|
|
282
|
-
| **Input Validation** | Sanitize user input
|
|
283
|
-
| **Cryptography**
|
|
284
|
-
| **Cryptography**
|
|
234
|
+
| **Data Storage** | Credentials stored securely | Keychain/KeyStore for tokens |
|
|
235
|
+
| **Data Storage** | Sensitive data encrypted | AES-256 for cached data |
|
|
236
|
+
| **Communication** | HTTPS only | `validatesSecureCertificate: true` |
|
|
237
|
+
| **Communication** | Certificate pinning | SSL pinning enabled |
|
|
238
|
+
| **Authentication** | Token refresh | Auto-refresh before expiry |
|
|
239
|
+
| **Authentication** | Session timeout | Auto-logout after inactivity |
|
|
240
|
+
| **Input Validation** | Server-side validation | Never trust client input |
|
|
241
|
+
| **Input Validation** | Sanitize user input | Remove XSS patterns |
|
|
242
|
+
| **Cryptography** | No hardcoded keys | Keys from secure storage |
|
|
243
|
+
| **Cryptography** | Use standard algorithms | AES-256, SHA-256 |
|
|
@@ -20,6 +20,7 @@ Create a single source of truth using Backbone.Events:
|
|
|
20
20
|
```javascript
|
|
21
21
|
// lib/services/stateStore.js
|
|
22
22
|
const Backbone = require('alloy/backbone')
|
|
23
|
+
const _ = require('alloy/underscore')._
|
|
23
24
|
|
|
24
25
|
class StateStore {
|
|
25
26
|
constructor() {
|
|
@@ -280,13 +281,13 @@ Ti.App.addEventListener('resume', () => {
|
|
|
280
281
|
|
|
281
282
|
## Anti-patterns
|
|
282
283
|
|
|
283
|
-
| Anti-Pattern
|
|
284
|
+
| Anti-Pattern | Why It's Bad | Solution |
|
|
284
285
|
| ------------------------------------- | ---------------------------- | ---------------------------------------- |
|
|
285
|
-
| `Ti.App.fireEvent` for state
|
|
286
|
-
| Direct collection mutation
|
|
287
|
-
| State in multiple places
|
|
288
|
-
| Global variables (`Alloy.Globals`)
|
|
289
|
-
| Controller-to-controller direct calls | Tight coupling
|
|
286
|
+
| `Ti.App.fireEvent` for state | No cleanup, memory leaks | Use StateStore with `offChange` |
|
|
287
|
+
| Direct collection mutation | Bypasses reactivity | Use collection methods (`add`, `remove`) |
|
|
288
|
+
| State in multiple places | Inconsistency bugs | Single source of truth |
|
|
289
|
+
| Global variables (`Alloy.Globals`) | No reactivity, hard to track | Use StateStore |
|
|
290
|
+
| Controller-to-controller direct calls | Tight coupling | Use StateStore or events |
|
|
290
291
|
|
|
291
292
|
## Persistence strategies
|
|
292
293
|
|
|
@@ -472,14 +473,14 @@ exports.appStore = {
|
|
|
472
473
|
|
|
473
474
|
### Choosing a strategy
|
|
474
475
|
|
|
475
|
-
| Data Type
|
|
476
|
-
| ------------------ | ------------------- |
|
|
477
|
-
| User preferences
|
|
478
|
-
| Auth tokens
|
|
479
|
-
| User profile
|
|
480
|
-
| Lists (100+ items) | SQLite
|
|
481
|
-
| Offline queue
|
|
482
|
-
| Cache
|
|
476
|
+
| Data Type | Strategy | Reason |
|
|
477
|
+
| ------------------ | ------------------- | ----------------------- |
|
|
478
|
+
| User preferences | Ti.App.Properties | Simple main-value, fast |
|
|
479
|
+
| Auth tokens | Keychain/KeyStore | Security |
|
|
480
|
+
| User profile | Properties + Secure | Mixed sensitivity |
|
|
481
|
+
| Lists (100+ items) | SQLite | Query, pagination |
|
|
482
|
+
| Offline queue | SQLite | Durability, FIFO |
|
|
483
|
+
| Cache | In-memory + SQLite | Speed + persistence |
|
|
483
484
|
|
|
484
485
|
## State middleware
|
|
485
486
|
|