native-update 1.0.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/CapacitorNativeUpdate.podspec +18 -0
- package/LICENSE +21 -0
- package/Readme.md +451 -0
- package/android/build.gradle +92 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +8 -0
- package/android/gradle.properties +17 -0
- package/android/proguard-rules.pro +29 -0
- package/android/settings.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +34 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppReviewPlugin.kt +153 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/AppUpdatePlugin.kt +275 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundNotificationManager.kt +390 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateManager.kt +46 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdatePlugin.kt +333 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/BackgroundUpdateWorker.kt +251 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/CapacitorNativeUpdatePlugin.kt +265 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/LiveUpdatePlugin.kt +526 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/NotificationActionReceiver.kt +99 -0
- package/android/src/main/java/com/aoneahsan/nativeupdate/SecurityManager.kt +249 -0
- package/dist/esm/__tests__/bundle-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/bundle-manager.test.js +123 -0
- package/dist/esm/__tests__/bundle-manager.test.js.map +1 -0
- package/dist/esm/__tests__/config.test.d.ts +1 -0
- package/dist/esm/__tests__/config.test.js +69 -0
- package/dist/esm/__tests__/config.test.js.map +1 -0
- package/dist/esm/__tests__/integration.test.d.ts +1 -0
- package/dist/esm/__tests__/integration.test.js +78 -0
- package/dist/esm/__tests__/integration.test.js.map +1 -0
- package/dist/esm/__tests__/security.test.d.ts +1 -0
- package/dist/esm/__tests__/security.test.js +54 -0
- package/dist/esm/__tests__/security.test.js.map +1 -0
- package/dist/esm/__tests__/version-manager.test.d.ts +1 -0
- package/dist/esm/__tests__/version-manager.test.js +45 -0
- package/dist/esm/__tests__/version-manager.test.js.map +1 -0
- package/dist/esm/app-review/app-review-manager.d.ts +24 -0
- package/dist/esm/app-review/app-review-manager.js +195 -0
- package/dist/esm/app-review/app-review-manager.js.map +1 -0
- package/dist/esm/app-review/index.d.ts +5 -0
- package/dist/esm/app-review/index.js +6 -0
- package/dist/esm/app-review/index.js.map +1 -0
- package/dist/esm/app-review/platform-review-handler.d.ts +20 -0
- package/dist/esm/app-review/platform-review-handler.js +138 -0
- package/dist/esm/app-review/platform-review-handler.js.map +1 -0
- package/dist/esm/app-review/review-conditions-checker.d.ts +22 -0
- package/dist/esm/app-review/review-conditions-checker.js +155 -0
- package/dist/esm/app-review/review-conditions-checker.js.map +1 -0
- package/dist/esm/app-review/review-rate-limiter.d.ts +23 -0
- package/dist/esm/app-review/review-rate-limiter.js +164 -0
- package/dist/esm/app-review/review-rate-limiter.js.map +1 -0
- package/dist/esm/app-review/types.d.ts +41 -0
- package/dist/esm/app-review/types.js +2 -0
- package/dist/esm/app-review/types.js.map +1 -0
- package/dist/esm/app-update/app-update-checker.d.ts +13 -0
- package/dist/esm/app-update/app-update-checker.js +104 -0
- package/dist/esm/app-update/app-update-checker.js.map +1 -0
- package/dist/esm/app-update/app-update-installer.d.ts +19 -0
- package/dist/esm/app-update/app-update-installer.js +123 -0
- package/dist/esm/app-update/app-update-installer.js.map +1 -0
- package/dist/esm/app-update/app-update-manager.d.ts +28 -0
- package/dist/esm/app-update/app-update-manager.js +199 -0
- package/dist/esm/app-update/app-update-manager.js.map +1 -0
- package/dist/esm/app-update/app-update-notifier.d.ts +14 -0
- package/dist/esm/app-update/app-update-notifier.js +100 -0
- package/dist/esm/app-update/app-update-notifier.js.map +1 -0
- package/dist/esm/app-update/index.d.ts +6 -0
- package/dist/esm/app-update/index.js +7 -0
- package/dist/esm/app-update/index.js.map +1 -0
- package/dist/esm/app-update/platform-app-update.d.ts +19 -0
- package/dist/esm/app-update/platform-app-update.js +129 -0
- package/dist/esm/app-update/platform-app-update.js.map +1 -0
- package/dist/esm/app-update/types.d.ts +58 -0
- package/dist/esm/app-update/types.js +12 -0
- package/dist/esm/app-update/types.js.map +1 -0
- package/dist/esm/background-update/background-scheduler.d.ts +17 -0
- package/dist/esm/background-update/background-scheduler.js +195 -0
- package/dist/esm/background-update/background-scheduler.js.map +1 -0
- package/dist/esm/background-update/index.d.ts +3 -0
- package/dist/esm/background-update/index.js +3 -0
- package/dist/esm/background-update/index.js.map +1 -0
- package/dist/esm/background-update/notification-manager.d.ts +29 -0
- package/dist/esm/background-update/notification-manager.js +89 -0
- package/dist/esm/background-update/notification-manager.js.map +1 -0
- package/dist/esm/core/analytics.d.ts +70 -0
- package/dist/esm/core/analytics.js +137 -0
- package/dist/esm/core/analytics.js.map +1 -0
- package/dist/esm/core/cache-manager.d.ts +72 -0
- package/dist/esm/core/cache-manager.js +275 -0
- package/dist/esm/core/cache-manager.js.map +1 -0
- package/dist/esm/core/config.d.ts +48 -0
- package/dist/esm/core/config.js +83 -0
- package/dist/esm/core/config.js.map +1 -0
- package/dist/esm/core/errors.d.ts +51 -0
- package/dist/esm/core/errors.js +80 -0
- package/dist/esm/core/errors.js.map +1 -0
- package/dist/esm/core/logger.d.ts +21 -0
- package/dist/esm/core/logger.js +109 -0
- package/dist/esm/core/logger.js.map +1 -0
- package/dist/esm/core/performance.d.ts +53 -0
- package/dist/esm/core/performance.js +140 -0
- package/dist/esm/core/performance.js.map +1 -0
- package/dist/esm/core/plugin-manager.d.ts +66 -0
- package/dist/esm/core/plugin-manager.js +148 -0
- package/dist/esm/core/plugin-manager.js.map +1 -0
- package/dist/esm/core/security.d.ts +93 -0
- package/dist/esm/core/security.js +315 -0
- package/dist/esm/core/security.js.map +1 -0
- package/dist/esm/definitions.d.ts +639 -0
- package/dist/esm/definitions.js +103 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/live-update/bundle-manager.d.ts +94 -0
- package/dist/esm/live-update/bundle-manager.js +310 -0
- package/dist/esm/live-update/bundle-manager.js.map +1 -0
- package/dist/esm/live-update/certificate-pinning.d.ts +38 -0
- package/dist/esm/live-update/certificate-pinning.js +78 -0
- package/dist/esm/live-update/certificate-pinning.js.map +1 -0
- package/dist/esm/live-update/download-manager.d.ts +67 -0
- package/dist/esm/live-update/download-manager.js +319 -0
- package/dist/esm/live-update/download-manager.js.map +1 -0
- package/dist/esm/live-update/update-manager.d.ts +52 -0
- package/dist/esm/live-update/update-manager.js +294 -0
- package/dist/esm/live-update/update-manager.js.map +1 -0
- package/dist/esm/live-update/version-manager.d.ts +84 -0
- package/dist/esm/live-update/version-manager.js +335 -0
- package/dist/esm/live-update/version-manager.js.map +1 -0
- package/dist/esm/plugin.d.ts +6 -0
- package/dist/esm/plugin.js +283 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/security/crypto.d.ts +25 -0
- package/dist/esm/security/crypto.js +70 -0
- package/dist/esm/security/crypto.js.map +1 -0
- package/dist/esm/security/validator.d.ts +60 -0
- package/dist/esm/security/validator.js +143 -0
- package/dist/esm/security/validator.js.map +1 -0
- package/dist/esm/web.d.ts +74 -0
- package/dist/esm/web.js +595 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +2 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.esm.js +2 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -0
- package/docs/APP_REVIEW_GUIDE.md +768 -0
- package/docs/BUNDLE_SIGNING.md +264 -0
- package/docs/LIVE_UPDATES_GUIDE.md +650 -0
- package/docs/MIGRATION.md +192 -0
- package/docs/NATIVE_UPDATES_GUIDE.md +694 -0
- package/docs/QUICK_START.md +606 -0
- package/docs/README.md +111 -0
- package/docs/REMAINING_FEATURES.md +139 -0
- package/docs/api/app-review-api.md +259 -0
- package/docs/api/app-update-api.md +238 -0
- package/docs/api/events-api.md +451 -0
- package/docs/api/live-update-api.md +265 -0
- package/docs/background-updates.md +392 -0
- package/docs/examples/advanced-scenarios.md +410 -0
- package/docs/examples/basic-usage.md +185 -0
- package/docs/features/app-reviews.md +975 -0
- package/docs/features/app-updates.md +785 -0
- package/docs/features/live-updates.md +633 -0
- package/docs/getting-started/configuration.md +468 -0
- package/docs/getting-started/installation.md +209 -0
- package/docs/getting-started/quick-start.md +379 -0
- package/docs/guides/deployment-guide.md +333 -0
- package/docs/guides/migration-from-codepush.md +142 -0
- package/docs/guides/security-best-practices.md +1057 -0
- package/docs/guides/testing-guide.md +373 -0
- package/docs/production-readiness.md +478 -0
- package/docs/security/certificate-pinning.md +122 -0
- package/docs/server-requirements.md +147 -0
- package/ios/Plugin/AppReview/AppReviewPlugin.swift +158 -0
- package/ios/Plugin/AppUpdate/AppUpdatePlugin.swift +234 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundNotificationManager.swift +329 -0
- package/ios/Plugin/BackgroundUpdate/BackgroundUpdatePlugin.swift +396 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.m +45 -0
- package/ios/Plugin/CapacitorNativeUpdatePlugin.swift +190 -0
- package/ios/Plugin/Info.plist +43 -0
- package/ios/Plugin/LiveUpdate/LiveUpdatePlugin.swift +689 -0
- package/ios/Plugin/LiveUpdate/WebViewConfiguration.swift +45 -0
- package/ios/Plugin/Security/SecurityManager.swift +289 -0
- package/package.json +90 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Backend Server Requirements
|
|
2
|
+
|
|
3
|
+
This document outlines the server infrastructure you need to build to use this plugin.
|
|
4
|
+
|
|
5
|
+
## ⚠️ No Server Included
|
|
6
|
+
|
|
7
|
+
This plugin does **not** include a backend server. You must build your own server that implements these requirements.
|
|
8
|
+
|
|
9
|
+
## Required API Endpoints
|
|
10
|
+
|
|
11
|
+
### 1. Version Check Endpoint
|
|
12
|
+
```
|
|
13
|
+
GET /api/v1/updates/check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Query Parameters:**
|
|
17
|
+
- `appId`: Your application ID
|
|
18
|
+
- `currentVersion`: Current bundle version
|
|
19
|
+
- `platform`: ios | android | web
|
|
20
|
+
- `channel`: production | staging | development
|
|
21
|
+
|
|
22
|
+
**Response:**
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"updateAvailable": true,
|
|
26
|
+
"version": "1.2.3",
|
|
27
|
+
"minAppVersion": "2.0.0",
|
|
28
|
+
"releaseNotes": "Bug fixes and improvements",
|
|
29
|
+
"downloadUrl": "https://cdn.example.com/bundles/1.2.3.zip",
|
|
30
|
+
"signature": "base64-encoded-signature",
|
|
31
|
+
"checksum": "sha256-hash",
|
|
32
|
+
"size": 1234567,
|
|
33
|
+
"mandatory": false
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Bundle Download Endpoint
|
|
38
|
+
```
|
|
39
|
+
GET /api/v1/bundles/{version}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Requirements:**
|
|
43
|
+
- Serve bundle files with proper MIME types
|
|
44
|
+
- Support resume/partial downloads
|
|
45
|
+
- Include security headers
|
|
46
|
+
- CDN integration recommended
|
|
47
|
+
|
|
48
|
+
### 3. Update Confirmation Endpoint
|
|
49
|
+
```
|
|
50
|
+
POST /api/v1/updates/confirm
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Body:**
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"appId": "com.example.app",
|
|
57
|
+
"version": "1.2.3",
|
|
58
|
+
"platform": "ios",
|
|
59
|
+
"success": true,
|
|
60
|
+
"deviceId": "unique-device-id"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Security Requirements
|
|
65
|
+
|
|
66
|
+
### Bundle Signing
|
|
67
|
+
- Generate RSA or ECDSA key pairs
|
|
68
|
+
- Sign bundles with private key
|
|
69
|
+
- Include public key in app
|
|
70
|
+
- Verify signatures before applying updates
|
|
71
|
+
|
|
72
|
+
### HTTPS Requirements
|
|
73
|
+
- All endpoints must use HTTPS
|
|
74
|
+
- Consider certificate pinning
|
|
75
|
+
- Implement rate limiting
|
|
76
|
+
- Add authentication tokens
|
|
77
|
+
|
|
78
|
+
## Storage Infrastructure
|
|
79
|
+
|
|
80
|
+
### Bundle Storage
|
|
81
|
+
- Store bundle files securely
|
|
82
|
+
- Implement versioning system
|
|
83
|
+
- Clean up old versions
|
|
84
|
+
- Monitor storage usage
|
|
85
|
+
|
|
86
|
+
### CDN Configuration
|
|
87
|
+
- Geographic distribution
|
|
88
|
+
- Caching strategies
|
|
89
|
+
- Bandwidth optimization
|
|
90
|
+
- Fallback mechanisms
|
|
91
|
+
|
|
92
|
+
## Example Server Structure
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
update-server/
|
|
96
|
+
├── src/
|
|
97
|
+
│ ├── api/
|
|
98
|
+
│ │ ├── updates.js
|
|
99
|
+
│ │ ├── bundles.js
|
|
100
|
+
│ │ └── analytics.js
|
|
101
|
+
│ ├── services/
|
|
102
|
+
│ │ ├── signing.js
|
|
103
|
+
│ │ ├── storage.js
|
|
104
|
+
│ │ └── version.js
|
|
105
|
+
│ ├── middleware/
|
|
106
|
+
│ │ ├── auth.js
|
|
107
|
+
│ │ ├── security.js
|
|
108
|
+
│ │ └── logging.js
|
|
109
|
+
│ └── config/
|
|
110
|
+
│ └── index.js
|
|
111
|
+
├── bundles/
|
|
112
|
+
│ └── [stored bundle files]
|
|
113
|
+
├── keys/
|
|
114
|
+
│ ├── private.pem
|
|
115
|
+
│ └── public.pem
|
|
116
|
+
└── package.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Minimum Implementation Checklist
|
|
120
|
+
|
|
121
|
+
- [ ] Version check API endpoint
|
|
122
|
+
- [ ] Bundle download endpoint
|
|
123
|
+
- [ ] Bundle signing implementation
|
|
124
|
+
- [ ] HTTPS configuration
|
|
125
|
+
- [ ] Basic authentication
|
|
126
|
+
- [ ] Error handling
|
|
127
|
+
- [ ] Logging system
|
|
128
|
+
- [ ] Health check endpoint
|
|
129
|
+
|
|
130
|
+
## Recommended Features
|
|
131
|
+
|
|
132
|
+
- [ ] Analytics tracking
|
|
133
|
+
- [ ] A/B testing support
|
|
134
|
+
- [ ] Rollback capabilities
|
|
135
|
+
- [ ] Multi-tenant support
|
|
136
|
+
- [ ] Admin dashboard
|
|
137
|
+
- [ ] Monitoring/alerting
|
|
138
|
+
- [ ] Automated testing
|
|
139
|
+
|
|
140
|
+
## Getting Help
|
|
141
|
+
|
|
142
|
+
Since no server is provided, you'll need to:
|
|
143
|
+
1. Build your own implementation
|
|
144
|
+
2. Use a third-party service
|
|
145
|
+
3. Hire developers familiar with update servers
|
|
146
|
+
|
|
147
|
+
Consider looking at open-source examples or commercial solutions for inspiration.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import StoreKit
|
|
4
|
+
|
|
5
|
+
class AppReviewPlugin {
|
|
6
|
+
private weak var plugin: CAPPlugin?
|
|
7
|
+
private var config: [String: Any]?
|
|
8
|
+
private let userDefaults = UserDefaults.standard
|
|
9
|
+
|
|
10
|
+
private struct Keys {
|
|
11
|
+
static let installDate = "native_update_review_install_date"
|
|
12
|
+
static let lastReviewRequest = "native_update_review_last_request"
|
|
13
|
+
static let launchCount = "native_update_review_launch_count"
|
|
14
|
+
static let reviewShownCount = "native_update_review_shown_count"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init(plugin: CAPPlugin) {
|
|
18
|
+
self.plugin = plugin
|
|
19
|
+
|
|
20
|
+
// Initialize install date if not set
|
|
21
|
+
if userDefaults.object(forKey: Keys.installDate) == nil {
|
|
22
|
+
userDefaults.set(Date(), forKey: Keys.installDate)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Increment launch count
|
|
26
|
+
incrementLaunchCount()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func configure(_ config: [String: Any]) throws {
|
|
30
|
+
self.config = config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func requestReview(_ call: CAPPluginCall) {
|
|
34
|
+
// First check if we can request a review
|
|
35
|
+
let (allowed, reason) = checkCanRequestReview()
|
|
36
|
+
|
|
37
|
+
if !allowed {
|
|
38
|
+
call.resolve([
|
|
39
|
+
"shown": false,
|
|
40
|
+
"error": reason ?? "Review conditions not met"
|
|
41
|
+
])
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Request review using StoreKit
|
|
46
|
+
DispatchQueue.main.async { [weak self] in
|
|
47
|
+
if #available(iOS 14.0, *) {
|
|
48
|
+
if let windowScene = UIApplication.shared.connectedScenes
|
|
49
|
+
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
|
50
|
+
SKStoreReviewController.requestReview(in: windowScene)
|
|
51
|
+
self?.updateReviewRequestTime()
|
|
52
|
+
|
|
53
|
+
call.resolve([
|
|
54
|
+
"shown": true
|
|
55
|
+
])
|
|
56
|
+
} else {
|
|
57
|
+
call.resolve([
|
|
58
|
+
"shown": false,
|
|
59
|
+
"error": "No active window scene found"
|
|
60
|
+
])
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Fallback for iOS 13 and earlier
|
|
64
|
+
SKStoreReviewController.requestReview()
|
|
65
|
+
self?.updateReviewRequestTime()
|
|
66
|
+
|
|
67
|
+
call.resolve([
|
|
68
|
+
"shown": true
|
|
69
|
+
])
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func canRequestReview(_ call: CAPPluginCall) {
|
|
75
|
+
let (allowed, reason) = checkCanRequestReview()
|
|
76
|
+
|
|
77
|
+
var result: [String: Any] = ["allowed": allowed]
|
|
78
|
+
if !allowed, let reason = reason {
|
|
79
|
+
result["reason"] = reason
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
call.resolve(result)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - Private Methods
|
|
86
|
+
|
|
87
|
+
private func checkCanRequestReview() -> (Bool, String?) {
|
|
88
|
+
// Check if debug mode is enabled
|
|
89
|
+
let debugMode = config?["debugMode"] as? Bool ?? false
|
|
90
|
+
if debugMode {
|
|
91
|
+
return (true, nil)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let now = Date()
|
|
95
|
+
let installDate = userDefaults.object(forKey: Keys.installDate) as? Date ?? now
|
|
96
|
+
let lastReviewRequest = userDefaults.object(forKey: Keys.lastReviewRequest) as? Date
|
|
97
|
+
let launchCount = userDefaults.integer(forKey: Keys.launchCount)
|
|
98
|
+
let shownCount = userDefaults.integer(forKey: Keys.reviewShownCount)
|
|
99
|
+
|
|
100
|
+
// Check minimum days since install
|
|
101
|
+
let minDaysSinceInstall = config?["minimumDaysSinceInstall"] as? Int ?? 7
|
|
102
|
+
let daysSinceInstall = Calendar.current.dateComponents([.day], from: installDate, to: now).day ?? 0
|
|
103
|
+
if daysSinceInstall < minDaysSinceInstall {
|
|
104
|
+
return (false, "Not enough days since install")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check minimum days since last prompt
|
|
108
|
+
if let lastRequest = lastReviewRequest {
|
|
109
|
+
let minDaysSinceLastPrompt = config?["minimumDaysSinceLastPrompt"] as? Int ?? 90
|
|
110
|
+
let daysSinceLastPrompt = Calendar.current.dateComponents([.day], from: lastRequest, to: now).day ?? 0
|
|
111
|
+
if daysSinceLastPrompt < minDaysSinceLastPrompt {
|
|
112
|
+
return (false, "Too soon since last review request")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check minimum launch count
|
|
117
|
+
let minLaunchCount = config?["minimumLaunchCount"] as? Int ?? 3
|
|
118
|
+
if launchCount < minLaunchCount {
|
|
119
|
+
return (false, "Not enough app launches")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// iOS limits to 3 review requests per year
|
|
123
|
+
if shownCount >= 3 {
|
|
124
|
+
// Check if it's been a year since the first request
|
|
125
|
+
if let firstRequestDate = userDefaults.object(forKey: "native_update_review_first_request") as? Date {
|
|
126
|
+
let daysSinceFirst = Calendar.current.dateComponents([.day], from: firstRequestDate, to: now).day ?? 0
|
|
127
|
+
if daysSinceFirst < 365 {
|
|
128
|
+
return (false, "Review quota exceeded")
|
|
129
|
+
} else {
|
|
130
|
+
// Reset count after a year
|
|
131
|
+
userDefaults.set(0, forKey: Keys.reviewShownCount)
|
|
132
|
+
userDefaults.removeObject(forKey: "native_update_review_first_request")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (true, nil)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func incrementLaunchCount() {
|
|
141
|
+
let currentCount = userDefaults.integer(forKey: Keys.launchCount)
|
|
142
|
+
userDefaults.set(currentCount + 1, forKey: Keys.launchCount)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func updateReviewRequestTime() {
|
|
146
|
+
let now = Date()
|
|
147
|
+
userDefaults.set(now, forKey: Keys.lastReviewRequest)
|
|
148
|
+
|
|
149
|
+
// Track first request for yearly quota
|
|
150
|
+
if userDefaults.object(forKey: "native_update_review_first_request") == nil {
|
|
151
|
+
userDefaults.set(now, forKey: "native_update_review_first_request")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Increment shown count
|
|
155
|
+
let shownCount = userDefaults.integer(forKey: Keys.reviewShownCount)
|
|
156
|
+
userDefaults.set(shownCount + 1, forKey: Keys.reviewShownCount)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
class AppUpdatePlugin {
|
|
6
|
+
private weak var plugin: CAPPlugin?
|
|
7
|
+
private var config: [String: Any]?
|
|
8
|
+
private let iTunesLookupURL = "https://itunes.apple.com/lookup"
|
|
9
|
+
|
|
10
|
+
init(plugin: CAPPlugin) {
|
|
11
|
+
self.plugin = plugin
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func configure(_ config: [String: Any]) throws {
|
|
15
|
+
self.config = config
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func getAppUpdateInfo(_ call: CAPPluginCall) {
|
|
19
|
+
Task {
|
|
20
|
+
do {
|
|
21
|
+
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
|
22
|
+
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
|
23
|
+
|
|
24
|
+
// Check iTunes for app info
|
|
25
|
+
let appInfo = try await checkAppStoreVersion(bundleId: bundleId)
|
|
26
|
+
|
|
27
|
+
let storeVersion = appInfo["version"] as? String ?? "0.0.0"
|
|
28
|
+
let updateAvailable = isVersionNewer(storeVersion, than: currentVersion)
|
|
29
|
+
|
|
30
|
+
var result: [String: Any] = [
|
|
31
|
+
"updateAvailable": updateAvailable,
|
|
32
|
+
"currentVersion": currentVersion
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
if updateAvailable {
|
|
36
|
+
result["availableVersion"] = storeVersion
|
|
37
|
+
|
|
38
|
+
// Calculate staleness in days
|
|
39
|
+
if let releaseDate = appInfo["currentVersionReleaseDate"] as? String {
|
|
40
|
+
let formatter = ISO8601DateFormatter()
|
|
41
|
+
if let date = formatter.date(from: releaseDate) {
|
|
42
|
+
let days = Calendar.current.dateComponents([.day], from: date, to: Date()).day ?? 0
|
|
43
|
+
result["clientVersionStalenessDays"] = days
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// iOS doesn't have the same update types as Android
|
|
49
|
+
result["immediateUpdateAllowed"] = false
|
|
50
|
+
result["flexibleUpdateAllowed"] = false
|
|
51
|
+
|
|
52
|
+
// Check minimum version requirement
|
|
53
|
+
if let minimumVersion = config?["minimumVersion"] as? String {
|
|
54
|
+
let needsUpdate = isVersionNewer(minimumVersion, than: currentVersion)
|
|
55
|
+
if needsUpdate {
|
|
56
|
+
result["updatePriority"] = 5 // High priority
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
call.resolve(result)
|
|
61
|
+
} catch {
|
|
62
|
+
call.reject("UPDATE_CHECK_FAILED", error.localizedDescription)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func performImmediateUpdate(_ call: CAPPluginCall) {
|
|
68
|
+
// iOS doesn't support in-app updates like Android
|
|
69
|
+
// We'll open the App Store instead
|
|
70
|
+
openAppStore(call)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func startFlexibleUpdate(_ call: CAPPluginCall) {
|
|
74
|
+
// iOS doesn't support flexible updates
|
|
75
|
+
call.reject("PLATFORM_NOT_SUPPORTED", "Flexible updates are not supported on iOS")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func completeFlexibleUpdate(_ call: CAPPluginCall) {
|
|
79
|
+
// iOS doesn't support flexible updates
|
|
80
|
+
call.reject("PLATFORM_NOT_SUPPORTED", "Flexible updates are not supported on iOS")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func openAppStore(_ call: CAPPluginCall) {
|
|
84
|
+
let appId = call.getString("appId") ?? getAppStoreId()
|
|
85
|
+
|
|
86
|
+
guard let appId = appId else {
|
|
87
|
+
call.reject("APP_ID_REQUIRED", "App ID is required to open App Store")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let urlString = "itms-apps://apple.com/app/id\(appId)"
|
|
92
|
+
|
|
93
|
+
DispatchQueue.main.async {
|
|
94
|
+
if let url = URL(string: urlString),
|
|
95
|
+
UIApplication.shared.canOpenURL(url) {
|
|
96
|
+
UIApplication.shared.open(url, options: [:]) { success in
|
|
97
|
+
if success {
|
|
98
|
+
call.resolve()
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback to web URL
|
|
101
|
+
if let webUrl = URL(string: "https://apps.apple.com/app/id\(appId)") {
|
|
102
|
+
UIApplication.shared.open(webUrl, options: [:]) { webSuccess in
|
|
103
|
+
if webSuccess {
|
|
104
|
+
call.resolve()
|
|
105
|
+
} else {
|
|
106
|
+
call.reject("OPEN_STORE_FAILED", "Failed to open App Store")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
call.reject("OPEN_STORE_FAILED", "Failed to open App Store")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
call.reject("INVALID_URL", "Invalid App Store URL")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// MARK: - Async Methods for Background Updates
|
|
121
|
+
|
|
122
|
+
func getAppUpdateInfoAsync() async -> AppUpdateInfo? {
|
|
123
|
+
do {
|
|
124
|
+
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
|
125
|
+
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0"
|
|
126
|
+
|
|
127
|
+
// Check iTunes for app info
|
|
128
|
+
let appInfo = try await checkAppStoreVersion(bundleId: bundleId)
|
|
129
|
+
|
|
130
|
+
let storeVersion = appInfo["version"] as? String ?? "0.0.0"
|
|
131
|
+
let updateAvailable = isVersionNewer(storeVersion, than: currentVersion)
|
|
132
|
+
|
|
133
|
+
return AppUpdateInfo(
|
|
134
|
+
updateAvailable: updateAvailable,
|
|
135
|
+
currentVersion: currentVersion,
|
|
136
|
+
availableVersion: updateAvailable ? storeVersion : nil
|
|
137
|
+
)
|
|
138
|
+
} catch {
|
|
139
|
+
NSLog("Failed to check app update: \(error.localizedDescription)")
|
|
140
|
+
return nil
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MARK: - Private Methods
|
|
145
|
+
|
|
146
|
+
private func checkAppStoreVersion(bundleId: String) async throws -> [String: Any] {
|
|
147
|
+
guard let countryCode = Locale.current.regionCode else {
|
|
148
|
+
throw NSError(domain: "AppUpdatePlugin", code: 1, userInfo: [
|
|
149
|
+
NSLocalizedDescriptionKey: "Could not determine country code"
|
|
150
|
+
])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var components = URLComponents(string: iTunesLookupURL)!
|
|
154
|
+
components.queryItems = [
|
|
155
|
+
URLQueryItem(name: "bundleId", value: bundleId),
|
|
156
|
+
URLQueryItem(name: "country", value: countryCode)
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
guard let url = components.url else {
|
|
160
|
+
throw NSError(domain: "AppUpdatePlugin", code: 2, userInfo: [
|
|
161
|
+
NSLocalizedDescriptionKey: "Invalid iTunes lookup URL"
|
|
162
|
+
])
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
166
|
+
|
|
167
|
+
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
168
|
+
let results = json["results"] as? [[String: Any]],
|
|
169
|
+
let appInfo = results.first else {
|
|
170
|
+
throw NSError(domain: "AppUpdatePlugin", code: 3, userInfo: [
|
|
171
|
+
NSLocalizedDescriptionKey: "No app found in App Store"
|
|
172
|
+
])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return appInfo
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func isVersionNewer(_ version1: String, than version2: String) -> Bool {
|
|
179
|
+
let v1Components = version1.split(separator: ".").compactMap { Int($0) }
|
|
180
|
+
let v2Components = version2.split(separator: ".").compactMap { Int($0) }
|
|
181
|
+
|
|
182
|
+
let maxCount = max(v1Components.count, v2Components.count)
|
|
183
|
+
|
|
184
|
+
for i in 0..<maxCount {
|
|
185
|
+
let v1 = i < v1Components.count ? v1Components[i] : 0
|
|
186
|
+
let v2 = i < v2Components.count ? v2Components[i] : 0
|
|
187
|
+
|
|
188
|
+
if v1 > v2 {
|
|
189
|
+
return true
|
|
190
|
+
} else if v1 < v2 {
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private func getAppStoreId() -> String? {
|
|
199
|
+
// Try to get from config first
|
|
200
|
+
if let storeUrl = (config?["storeUrl"] as? [String: Any])?["ios"] as? String {
|
|
201
|
+
// Extract ID from URL like https://apps.apple.com/app/id123456789
|
|
202
|
+
let pattern = #"id(\d+)"#
|
|
203
|
+
if let regex = try? NSRegularExpression(pattern: pattern),
|
|
204
|
+
let match = regex.firstMatch(in: storeUrl, range: NSRange(storeUrl.startIndex..., in: storeUrl)) {
|
|
205
|
+
let range = Range(match.range(at: 1), in: storeUrl)!
|
|
206
|
+
return String(storeUrl[range])
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Try to get from Info.plist
|
|
211
|
+
return Bundle.main.infoDictionary?["AppStoreId"] as? String
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// MARK: - Data Models
|
|
216
|
+
|
|
217
|
+
struct AppUpdateInfo {
|
|
218
|
+
let updateAvailable: Bool
|
|
219
|
+
let currentVersion: String
|
|
220
|
+
let availableVersion: String?
|
|
221
|
+
|
|
222
|
+
func toDictionary() -> [String: Any] {
|
|
223
|
+
var obj: [String: Any] = [
|
|
224
|
+
"updateAvailable": updateAvailable,
|
|
225
|
+
"currentVersion": currentVersion
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
if let availableVersion = availableVersion {
|
|
229
|
+
obj["availableVersion"] = availableVersion
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return obj
|
|
233
|
+
}
|
|
234
|
+
}
|