@swiftpatch/react-native 2.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.
Files changed (182) hide show
  1. package/README.md +430 -0
  2. package/android/build.gradle +105 -0
  3. package/android/src/main/AndroidManifest.xml +6 -0
  4. package/android/src/main/java/com/swiftpatch/BundleManager.kt +107 -0
  5. package/android/src/main/java/com/swiftpatch/CrashDetector.kt +79 -0
  6. package/android/src/main/java/com/swiftpatch/CryptoVerifier.kt +69 -0
  7. package/android/src/main/java/com/swiftpatch/DownloadManager.kt +120 -0
  8. package/android/src/main/java/com/swiftpatch/EventQueue.kt +86 -0
  9. package/android/src/main/java/com/swiftpatch/FileUtils.kt +60 -0
  10. package/android/src/main/java/com/swiftpatch/PatchApplier.kt +60 -0
  11. package/android/src/main/java/com/swiftpatch/SignalCrashHandler.kt +84 -0
  12. package/android/src/main/java/com/swiftpatch/SlotManager.kt +299 -0
  13. package/android/src/main/java/com/swiftpatch/SwiftPatchModule.kt +630 -0
  14. package/android/src/main/java/com/swiftpatch/SwiftPatchPackage.kt +21 -0
  15. package/android/src/main/jni/CMakeLists.txt +12 -0
  16. package/android/src/main/jni/bspatch.c +188 -0
  17. package/android/src/main/jni/bspatch.h +57 -0
  18. package/android/src/main/jni/bspatch_jni.c +28 -0
  19. package/ios/Libraries/bspatch/bspatch.c +188 -0
  20. package/ios/Libraries/bspatch/bspatch.h +50 -0
  21. package/ios/Libraries/bspatch/module.modulemap +4 -0
  22. package/ios/SwiftPatch/BundleManager.swift +113 -0
  23. package/ios/SwiftPatch/CrashDetector.swift +71 -0
  24. package/ios/SwiftPatch/CryptoVerifier.swift +70 -0
  25. package/ios/SwiftPatch/DownloadManager.swift +125 -0
  26. package/ios/SwiftPatch/EventQueue.swift +116 -0
  27. package/ios/SwiftPatch/FileUtils.swift +38 -0
  28. package/ios/SwiftPatch/PatchApplier.swift +41 -0
  29. package/ios/SwiftPatch/SignalCrashHandler.swift +129 -0
  30. package/ios/SwiftPatch/SlotManager.swift +360 -0
  31. package/ios/SwiftPatch/SwiftPatchModule.m +56 -0
  32. package/ios/SwiftPatch/SwiftPatchModule.swift +621 -0
  33. package/lib/commonjs/SwiftPatchCore.js +140 -0
  34. package/lib/commonjs/SwiftPatchCore.js.map +1 -0
  35. package/lib/commonjs/SwiftPatchProvider.js +617 -0
  36. package/lib/commonjs/SwiftPatchProvider.js.map +1 -0
  37. package/lib/commonjs/constants.js +50 -0
  38. package/lib/commonjs/constants.js.map +1 -0
  39. package/lib/commonjs/core/Downloader.js +63 -0
  40. package/lib/commonjs/core/Downloader.js.map +1 -0
  41. package/lib/commonjs/core/Installer.js +46 -0
  42. package/lib/commonjs/core/Installer.js.map +1 -0
  43. package/lib/commonjs/core/Rollback.js +36 -0
  44. package/lib/commonjs/core/Rollback.js.map +1 -0
  45. package/lib/commonjs/core/UpdateChecker.js +57 -0
  46. package/lib/commonjs/core/UpdateChecker.js.map +1 -0
  47. package/lib/commonjs/core/Verifier.js +82 -0
  48. package/lib/commonjs/core/Verifier.js.map +1 -0
  49. package/lib/commonjs/core/index.js +41 -0
  50. package/lib/commonjs/core/index.js.map +1 -0
  51. package/lib/commonjs/index.js +154 -0
  52. package/lib/commonjs/index.js.map +1 -0
  53. package/lib/commonjs/modal/SwiftPatchModal.js +667 -0
  54. package/lib/commonjs/modal/SwiftPatchModal.js.map +1 -0
  55. package/lib/commonjs/modal/useSwiftPatchModal.js +26 -0
  56. package/lib/commonjs/modal/useSwiftPatchModal.js.map +1 -0
  57. package/lib/commonjs/native/NativeSwiftPatch.js +85 -0
  58. package/lib/commonjs/native/NativeSwiftPatch.js.map +1 -0
  59. package/lib/commonjs/native/NativeSwiftPatchSpec.js +15 -0
  60. package/lib/commonjs/native/NativeSwiftPatchSpec.js.map +1 -0
  61. package/lib/commonjs/package.json +1 -0
  62. package/lib/commonjs/types.js +126 -0
  63. package/lib/commonjs/types.js.map +1 -0
  64. package/lib/commonjs/useSwiftPatch.js +31 -0
  65. package/lib/commonjs/useSwiftPatch.js.map +1 -0
  66. package/lib/commonjs/utils/api.js +206 -0
  67. package/lib/commonjs/utils/api.js.map +1 -0
  68. package/lib/commonjs/utils/device.js +23 -0
  69. package/lib/commonjs/utils/device.js.map +1 -0
  70. package/lib/commonjs/utils/logger.js +30 -0
  71. package/lib/commonjs/utils/logger.js.map +1 -0
  72. package/lib/commonjs/utils/storage.js +31 -0
  73. package/lib/commonjs/utils/storage.js.map +1 -0
  74. package/lib/commonjs/withSwiftPatch.js +42 -0
  75. package/lib/commonjs/withSwiftPatch.js.map +1 -0
  76. package/lib/module/SwiftPatchCore.js +135 -0
  77. package/lib/module/SwiftPatchCore.js.map +1 -0
  78. package/lib/module/SwiftPatchProvider.js +611 -0
  79. package/lib/module/SwiftPatchProvider.js.map +1 -0
  80. package/lib/module/constants.js +46 -0
  81. package/lib/module/constants.js.map +1 -0
  82. package/lib/module/core/Downloader.js +57 -0
  83. package/lib/module/core/Downloader.js.map +1 -0
  84. package/lib/module/core/Installer.js +41 -0
  85. package/lib/module/core/Installer.js.map +1 -0
  86. package/lib/module/core/Rollback.js +31 -0
  87. package/lib/module/core/Rollback.js.map +1 -0
  88. package/lib/module/core/UpdateChecker.js +51 -0
  89. package/lib/module/core/UpdateChecker.js.map +1 -0
  90. package/lib/module/core/Verifier.js +76 -0
  91. package/lib/module/core/Verifier.js.map +1 -0
  92. package/lib/module/core/index.js +8 -0
  93. package/lib/module/core/index.js.map +1 -0
  94. package/lib/module/index.js +34 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/modal/SwiftPatchModal.js +661 -0
  97. package/lib/module/modal/SwiftPatchModal.js.map +1 -0
  98. package/lib/module/modal/useSwiftPatchModal.js +22 -0
  99. package/lib/module/modal/useSwiftPatchModal.js.map +1 -0
  100. package/lib/module/native/NativeSwiftPatch.js +78 -0
  101. package/lib/module/native/NativeSwiftPatch.js.map +1 -0
  102. package/lib/module/native/NativeSwiftPatchSpec.js +12 -0
  103. package/lib/module/native/NativeSwiftPatchSpec.js.map +1 -0
  104. package/lib/module/types.js +139 -0
  105. package/lib/module/types.js.map +1 -0
  106. package/lib/module/useSwiftPatch.js +26 -0
  107. package/lib/module/useSwiftPatch.js.map +1 -0
  108. package/lib/module/utils/api.js +197 -0
  109. package/lib/module/utils/api.js.map +1 -0
  110. package/lib/module/utils/device.js +18 -0
  111. package/lib/module/utils/device.js.map +1 -0
  112. package/lib/module/utils/logger.js +26 -0
  113. package/lib/module/utils/logger.js.map +1 -0
  114. package/lib/module/utils/storage.js +24 -0
  115. package/lib/module/utils/storage.js.map +1 -0
  116. package/lib/module/withSwiftPatch.js +37 -0
  117. package/lib/module/withSwiftPatch.js.map +1 -0
  118. package/lib/typescript/SwiftPatchCore.d.ts +64 -0
  119. package/lib/typescript/SwiftPatchCore.d.ts.map +1 -0
  120. package/lib/typescript/SwiftPatchProvider.d.ts +22 -0
  121. package/lib/typescript/SwiftPatchProvider.d.ts.map +1 -0
  122. package/lib/typescript/constants.d.ts +33 -0
  123. package/lib/typescript/constants.d.ts.map +1 -0
  124. package/lib/typescript/core/Downloader.d.ts +34 -0
  125. package/lib/typescript/core/Downloader.d.ts.map +1 -0
  126. package/lib/typescript/core/Installer.d.ts +25 -0
  127. package/lib/typescript/core/Installer.d.ts.map +1 -0
  128. package/lib/typescript/core/Rollback.d.ts +18 -0
  129. package/lib/typescript/core/Rollback.d.ts.map +1 -0
  130. package/lib/typescript/core/UpdateChecker.d.ts +27 -0
  131. package/lib/typescript/core/UpdateChecker.d.ts.map +1 -0
  132. package/lib/typescript/core/Verifier.d.ts +31 -0
  133. package/lib/typescript/core/Verifier.d.ts.map +1 -0
  134. package/lib/typescript/core/index.d.ts +8 -0
  135. package/lib/typescript/core/index.d.ts.map +1 -0
  136. package/lib/typescript/index.d.ts +13 -0
  137. package/lib/typescript/index.d.ts.map +1 -0
  138. package/lib/typescript/modal/SwiftPatchModal.d.ts +11 -0
  139. package/lib/typescript/modal/SwiftPatchModal.d.ts.map +1 -0
  140. package/lib/typescript/modal/useSwiftPatchModal.d.ts +7 -0
  141. package/lib/typescript/modal/useSwiftPatchModal.d.ts.map +1 -0
  142. package/lib/typescript/native/NativeSwiftPatch.d.ts +61 -0
  143. package/lib/typescript/native/NativeSwiftPatch.d.ts.map +1 -0
  144. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts +67 -0
  145. package/lib/typescript/native/NativeSwiftPatchSpec.d.ts.map +1 -0
  146. package/lib/typescript/types.d.ts +266 -0
  147. package/lib/typescript/types.d.ts.map +1 -0
  148. package/lib/typescript/useSwiftPatch.d.ts +12 -0
  149. package/lib/typescript/useSwiftPatch.d.ts.map +1 -0
  150. package/lib/typescript/utils/api.d.ts +87 -0
  151. package/lib/typescript/utils/api.d.ts.map +1 -0
  152. package/lib/typescript/utils/device.d.ts +9 -0
  153. package/lib/typescript/utils/device.d.ts.map +1 -0
  154. package/lib/typescript/utils/logger.d.ts +8 -0
  155. package/lib/typescript/utils/logger.d.ts.map +1 -0
  156. package/lib/typescript/utils/storage.d.ts +14 -0
  157. package/lib/typescript/utils/storage.d.ts.map +1 -0
  158. package/lib/typescript/withSwiftPatch.d.ts +12 -0
  159. package/lib/typescript/withSwiftPatch.d.ts.map +1 -0
  160. package/package.json +99 -0
  161. package/react-native-swiftpatch.podspec +50 -0
  162. package/src/SwiftPatchCore.ts +148 -0
  163. package/src/SwiftPatchProvider.tsx +514 -0
  164. package/src/constants.ts +49 -0
  165. package/src/core/Downloader.ts +74 -0
  166. package/src/core/Installer.ts +38 -0
  167. package/src/core/Rollback.ts +28 -0
  168. package/src/core/UpdateChecker.ts +70 -0
  169. package/src/core/Verifier.ts +92 -0
  170. package/src/core/index.ts +11 -0
  171. package/src/index.ts +64 -0
  172. package/src/modal/SwiftPatchModal.tsx +657 -0
  173. package/src/modal/useSwiftPatchModal.ts +24 -0
  174. package/src/native/NativeSwiftPatch.ts +205 -0
  175. package/src/native/NativeSwiftPatchSpec.ts +139 -0
  176. package/src/types.ts +336 -0
  177. package/src/useSwiftPatch.ts +29 -0
  178. package/src/utils/api.ts +244 -0
  179. package/src/utils/device.ts +15 -0
  180. package/src/utils/logger.ts +29 -0
  181. package/src/utils/storage.ts +23 -0
  182. package/src/withSwiftPatch.tsx +41 -0
@@ -0,0 +1,657 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ FlatList,
9
+ ActivityIndicator,
10
+ StyleSheet,
11
+ SafeAreaView,
12
+ ScrollView,
13
+ } from 'react-native';
14
+ import { useSwiftPatch } from '../useSwiftPatch';
15
+ import {
16
+ verifySDKPin,
17
+ listBuckets,
18
+ listBundles,
19
+ } from '../utils/api';
20
+ import NativeSwiftPatch from '../native/NativeSwiftPatch';
21
+ import { EnvironmentMode } from '../types';
22
+ import type { SlotMetadata } from '../types';
23
+
24
+ interface SwiftPatchModalProps {
25
+ visible: boolean;
26
+ onClose: () => void;
27
+ serverUrl: string;
28
+ deploymentKey: string;
29
+ customHeaders?: Record<string, string>;
30
+ }
31
+
32
+ type ModalScreen = 'login' | 'dashboard' | 'bundles';
33
+
34
+ interface BucketItem {
35
+ id: string;
36
+ name: string;
37
+ slug: string;
38
+ isDefault: boolean;
39
+ totalReleases: number;
40
+ }
41
+
42
+ interface BundleItem {
43
+ id: string;
44
+ version: string;
45
+ buildNumber: number;
46
+ platform: string;
47
+ bundleHash: string;
48
+ bundleSize: number;
49
+ releaseNote?: string;
50
+ status: string;
51
+ isMandatory: boolean;
52
+ createdAt: string;
53
+ }
54
+
55
+ export function SwiftPatchModal({
56
+ visible,
57
+ onClose,
58
+ serverUrl,
59
+ deploymentKey,
60
+ customHeaders = {},
61
+ }: SwiftPatchModalProps) {
62
+ const ctx = useSwiftPatch();
63
+
64
+ const [screen, setScreen] = useState<ModalScreen>('login');
65
+ const [sdkToken, setSdkToken] = useState<string | null>(null);
66
+ const [pin, setPin] = useState('');
67
+ const [loginError, setLoginError] = useState('');
68
+ const [isLoading, setIsLoading] = useState(false);
69
+ const [buckets, setBuckets] = useState<BucketItem[]>([]);
70
+ const [bundles, setBundles] = useState<BundleItem[]>([]);
71
+ const [selectedBucket, setSelectedBucket] = useState<BucketItem | null>(null);
72
+ const [hasMoreBundles, setHasMoreBundles] = useState(false);
73
+ const [pageOffset, setPageOffset] = useState(0);
74
+ const [downloadingHash, setDownloadingHash] = useState<string | null>(null);
75
+ const [downloadProgress, setDownloadProgress] = useState(0);
76
+ const [metadata, setMetadata] = useState<SlotMetadata | null>(null);
77
+
78
+ // Fetch metadata on open
79
+ useEffect(() => {
80
+ if (visible) {
81
+ NativeSwiftPatch.getSlotMetadata().then(setMetadata).catch(() => {});
82
+ }
83
+ }, [visible]);
84
+
85
+ // Login handler
86
+ const handleLogin = useCallback(async () => {
87
+ if (pin.length < 4) {
88
+ setLoginError('PIN must be at least 4 characters');
89
+ return;
90
+ }
91
+
92
+ setIsLoading(true);
93
+ setLoginError('');
94
+
95
+ const result = await verifySDKPin({
96
+ serverUrl,
97
+ deploymentKey,
98
+ pin,
99
+ customHeaders,
100
+ });
101
+
102
+ setIsLoading(false);
103
+
104
+ if (result) {
105
+ setSdkToken(result.sdkToken);
106
+ setScreen('dashboard');
107
+ // Fetch buckets
108
+ const bucketsData = await listBuckets({ serverUrl, sdkToken: result.sdkToken, deploymentKey, customHeaders });
109
+ setBuckets(bucketsData);
110
+ } else {
111
+ setLoginError('Invalid PIN. Please try again.');
112
+ }
113
+ }, [pin, serverUrl, deploymentKey, customHeaders]);
114
+
115
+ // Load bundles for a bucket
116
+ const loadBundles = useCallback(async (bucket: BucketItem, offset = 0) => {
117
+ if (!sdkToken) return;
118
+ setIsLoading(true);
119
+ setSelectedBucket(bucket);
120
+ setPageOffset(offset);
121
+
122
+ const result = await listBundles({
123
+ serverUrl,
124
+ sdkToken,
125
+ deploymentKey,
126
+ channelId: bucket.id,
127
+ pageOffset: offset,
128
+ customHeaders,
129
+ });
130
+
131
+ if (offset === 0) {
132
+ setBundles(result.bundles);
133
+ } else {
134
+ setBundles(prev => [...prev, ...result.bundles]);
135
+ }
136
+ setHasMoreBundles(result.hasMore);
137
+ setIsLoading(false);
138
+ setScreen('bundles');
139
+ }, [sdkToken, serverUrl, deploymentKey, customHeaders]);
140
+
141
+ // Download a specific bundle (stage)
142
+ const handleDownloadBundle = useCallback(async (bundle: BundleItem) => {
143
+ try {
144
+ setDownloadingHash(bundle.bundleHash);
145
+ setDownloadProgress(0);
146
+
147
+ // Download as stage bundle for testing
148
+ await ctx.downloadStageBundle(
149
+ `${serverUrl}/releases/${bundle.id}/download`,
150
+ bundle.bundleHash
151
+ );
152
+
153
+ setDownloadingHash(null);
154
+ setDownloadProgress(100);
155
+
156
+ // Refresh metadata
157
+ const meta = await NativeSwiftPatch.getSlotMetadata();
158
+ setMetadata(meta);
159
+ } catch (e) {
160
+ setDownloadingHash(null);
161
+ setDownloadProgress(0);
162
+ }
163
+ }, [ctx, serverUrl]);
164
+
165
+ // Switch environment
166
+ const handleSwitchEnvironment = useCallback(async () => {
167
+ const currentEnv = metadata?.environment || 'PROD';
168
+ const newEnv = currentEnv === 'PROD' ? EnvironmentMode.STAGING : EnvironmentMode.PRODUCTION;
169
+ const meta = await ctx.switchEnvironment(newEnv);
170
+ setMetadata(meta);
171
+ }, [ctx, metadata]);
172
+
173
+ // Stabilize
174
+ const handleStabilize = useCallback(async () => {
175
+ try {
176
+ const meta = await ctx.stabilize();
177
+ setMetadata(meta);
178
+ } catch (_e) {}
179
+ }, [ctx]);
180
+
181
+ // Rollback
182
+ const handleRollback = useCallback(async () => {
183
+ try {
184
+ await ctx.rollback();
185
+ const meta = await NativeSwiftPatch.getSlotMetadata();
186
+ setMetadata(meta);
187
+ } catch (_e) {}
188
+ }, [ctx]);
189
+
190
+ // Restart
191
+ const handleRestart = useCallback(() => {
192
+ ctx.restart();
193
+ }, [ctx]);
194
+
195
+ const handleClose = useCallback(() => {
196
+ setPin('');
197
+ setLoginError('');
198
+ setScreen(sdkToken ? 'dashboard' : 'login');
199
+ onClose();
200
+ }, [onClose, sdkToken]);
201
+
202
+ // ─── Render Login Screen ───────────────────────────────────
203
+
204
+ const renderLogin = () => (
205
+ <View style={styles.screenContainer}>
206
+ <Text style={styles.title}>SwiftPatch Dashboard</Text>
207
+ <Text style={styles.subtitle}>Enter your SDK PIN to access</Text>
208
+
209
+ <TextInput
210
+ style={styles.pinInput}
211
+ value={pin}
212
+ onChangeText={(text) => {
213
+ setPin(text.replace(/[^0-9a-zA-Z]/g, ''));
214
+ setLoginError('');
215
+ }}
216
+ placeholder="Enter PIN"
217
+ placeholderTextColor="#666"
218
+ secureTextEntry
219
+ maxLength={8}
220
+ autoFocus
221
+ />
222
+
223
+ {loginError ? <Text style={styles.errorText}>{loginError}</Text> : null}
224
+
225
+ <TouchableOpacity
226
+ style={[styles.button, pin.length < 4 && styles.buttonDisabled]}
227
+ onPress={handleLogin}
228
+ disabled={pin.length < 4 || isLoading}
229
+ >
230
+ {isLoading ? (
231
+ <ActivityIndicator color="#fff" size="small" />
232
+ ) : (
233
+ <Text style={styles.buttonText}>Unlock</Text>
234
+ )}
235
+ </TouchableOpacity>
236
+ </View>
237
+ );
238
+
239
+ // ─── Render Dashboard Screen ───────────────────────────────
240
+
241
+ const renderDashboard = () => (
242
+ <ScrollView style={styles.screenContainer}>
243
+ <Text style={styles.title}>Dashboard</Text>
244
+
245
+ {/* Current Status */}
246
+ <View style={styles.card}>
247
+ <Text style={styles.cardTitle}>Current Status</Text>
248
+ <View style={styles.row}>
249
+ <Text style={styles.label}>Environment:</Text>
250
+ <TouchableOpacity style={styles.envBadge} onPress={handleSwitchEnvironment}>
251
+ <Text style={styles.envBadgeText}>
252
+ {metadata?.environment || 'PROD'}
253
+ </Text>
254
+ </TouchableOpacity>
255
+ </View>
256
+ <View style={styles.row}>
257
+ <Text style={styles.label}>Slot:</Text>
258
+ <Text style={styles.value}>{metadata?.prod?.currentSlot || 'DEFAULT'}</Text>
259
+ </View>
260
+ <View style={styles.row}>
261
+ <Text style={styles.label}>New Hash:</Text>
262
+ <Text style={styles.value}>{metadata?.prod?.newHash?.substring(0, 12) || 'none'}...</Text>
263
+ </View>
264
+ <View style={styles.row}>
265
+ <Text style={styles.label}>Stable Hash:</Text>
266
+ <Text style={styles.value}>{metadata?.prod?.stableHash?.substring(0, 12) || 'none'}...</Text>
267
+ </View>
268
+ {metadata?.prod?.tempHash && (
269
+ <View style={styles.row}>
270
+ <Text style={styles.label}>Pending:</Text>
271
+ <Text style={[styles.value, { color: '#f59e0b' }]}>
272
+ {metadata.prod.tempHash.substring(0, 12)}... (restart required)
273
+ </Text>
274
+ </View>
275
+ )}
276
+ </View>
277
+
278
+ {/* Actions */}
279
+ <View style={styles.card}>
280
+ <Text style={styles.cardTitle}>Actions</Text>
281
+ <View style={styles.actionsRow}>
282
+ {metadata?.prod?.currentSlot === 'NEW_SLOT' && (
283
+ <TouchableOpacity style={styles.actionButton} onPress={handleStabilize}>
284
+ <Text style={styles.actionButtonText}>Stabilize</Text>
285
+ </TouchableOpacity>
286
+ )}
287
+ {metadata?.prod?.currentSlot !== 'DEFAULT_SLOT' && (
288
+ <TouchableOpacity style={[styles.actionButton, styles.dangerButton]} onPress={handleRollback}>
289
+ <Text style={styles.actionButtonText}>Rollback</Text>
290
+ </TouchableOpacity>
291
+ )}
292
+ {ctx.isRestartRequired && (
293
+ <TouchableOpacity style={[styles.actionButton, styles.successButton]} onPress={handleRestart}>
294
+ <Text style={styles.actionButtonText}>Restart</Text>
295
+ </TouchableOpacity>
296
+ )}
297
+ </View>
298
+ </View>
299
+
300
+ {/* Buckets */}
301
+ <View style={styles.card}>
302
+ <Text style={styles.cardTitle}>Channels</Text>
303
+ {buckets.length === 0 ? (
304
+ <Text style={styles.emptyText}>No channels found</Text>
305
+ ) : (
306
+ buckets.map((bucket) => (
307
+ <TouchableOpacity
308
+ key={bucket.id}
309
+ style={styles.bucketItem}
310
+ onPress={() => loadBundles(bucket)}
311
+ >
312
+ <View>
313
+ <Text style={styles.bucketName}>{bucket.name}</Text>
314
+ <Text style={styles.bucketMeta}>
315
+ {bucket.totalReleases} releases {bucket.isDefault ? '(default)' : ''}
316
+ </Text>
317
+ </View>
318
+ <Text style={styles.chevron}>›</Text>
319
+ </TouchableOpacity>
320
+ ))
321
+ )}
322
+ </View>
323
+ </ScrollView>
324
+ );
325
+
326
+ // ─── Render Bundles Screen ─────────────────────────────────
327
+
328
+ const renderBundles = () => (
329
+ <View style={styles.screenContainer}>
330
+ <TouchableOpacity onPress={() => setScreen('dashboard')} style={styles.backButton}>
331
+ <Text style={styles.backText}>‹ Back</Text>
332
+ </TouchableOpacity>
333
+
334
+ <Text style={styles.title}>{selectedBucket?.name || 'Bundles'}</Text>
335
+
336
+ <FlatList
337
+ data={bundles}
338
+ keyExtractor={(item) => item.id}
339
+ renderItem={({ item }) => (
340
+ <View style={styles.bundleCard}>
341
+ <View style={styles.bundleHeader}>
342
+ <Text style={styles.bundleVersion}>v{item.version} ({item.buildNumber})</Text>
343
+ <View style={[styles.statusBadge, item.status === 'RELEASED' ? styles.releasedBadge : styles.readyBadge]}>
344
+ <Text style={styles.statusBadgeText}>{item.status}</Text>
345
+ </View>
346
+ </View>
347
+ <Text style={styles.bundlePlatform}>{item.platform}</Text>
348
+ {item.releaseNote && <Text style={styles.bundleNote}>{item.releaseNote}</Text>}
349
+ <View style={styles.bundleFooter}>
350
+ <Text style={styles.bundleSize}>{formatSize(item.bundleSize)}</Text>
351
+ <TouchableOpacity
352
+ style={[styles.downloadButton, downloadingHash === item.bundleHash && styles.buttonDisabled]}
353
+ onPress={() => handleDownloadBundle(item)}
354
+ disabled={downloadingHash !== null}
355
+ >
356
+ {downloadingHash === item.bundleHash ? (
357
+ <ActivityIndicator color="#fff" size="small" />
358
+ ) : (
359
+ <Text style={styles.downloadButtonText}>Download</Text>
360
+ )}
361
+ </TouchableOpacity>
362
+ </View>
363
+ </View>
364
+ )}
365
+ ListEmptyComponent={
366
+ <Text style={styles.emptyText}>No bundles found</Text>
367
+ }
368
+ onEndReached={() => {
369
+ if (hasMoreBundles && !isLoading && selectedBucket) {
370
+ loadBundles(selectedBucket, pageOffset + 1);
371
+ }
372
+ }}
373
+ onEndReachedThreshold={0.5}
374
+ ListFooterComponent={
375
+ isLoading ? <ActivityIndicator style={{ margin: 20 }} color="#007AFF" /> : null
376
+ }
377
+ />
378
+ </View>
379
+ );
380
+
381
+ // ─── Main Render ───────────────────────────────────────────
382
+
383
+ return (
384
+ <Modal
385
+ visible={visible}
386
+ animationType="slide"
387
+ presentationStyle="pageSheet"
388
+ onRequestClose={handleClose}
389
+ >
390
+ <SafeAreaView style={styles.container}>
391
+ {/* Header */}
392
+ <View style={styles.header}>
393
+ <Text style={styles.headerTitle}>SwiftPatch</Text>
394
+ <TouchableOpacity onPress={handleClose}>
395
+ <Text style={styles.closeButton}>✕</Text>
396
+ </TouchableOpacity>
397
+ </View>
398
+
399
+ {/* Download Progress Overlay */}
400
+ {downloadingHash && (
401
+ <View style={styles.progressBar}>
402
+ <View style={[styles.progressFill, { width: `${downloadProgress}%` }]} />
403
+ </View>
404
+ )}
405
+
406
+ {/* Content */}
407
+ {screen === 'login' && renderLogin()}
408
+ {screen === 'dashboard' && renderDashboard()}
409
+ {screen === 'bundles' && renderBundles()}
410
+ </SafeAreaView>
411
+ </Modal>
412
+ );
413
+ }
414
+
415
+ function formatSize(bytes: number): string {
416
+ if (bytes < 1024) return `${bytes} B`;
417
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
418
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
419
+ }
420
+
421
+ const styles = StyleSheet.create({
422
+ container: {
423
+ flex: 1,
424
+ backgroundColor: '#0f0f23',
425
+ },
426
+ header: {
427
+ flexDirection: 'row',
428
+ justifyContent: 'space-between',
429
+ alignItems: 'center',
430
+ paddingHorizontal: 20,
431
+ paddingVertical: 16,
432
+ borderBottomWidth: 1,
433
+ borderBottomColor: '#1a1a3e',
434
+ },
435
+ headerTitle: {
436
+ fontSize: 18,
437
+ fontWeight: '700',
438
+ color: '#fff',
439
+ },
440
+ closeButton: {
441
+ fontSize: 20,
442
+ color: '#888',
443
+ padding: 4,
444
+ },
445
+ progressBar: {
446
+ height: 3,
447
+ backgroundColor: '#1a1a3e',
448
+ },
449
+ progressFill: {
450
+ height: '100%',
451
+ backgroundColor: '#007AFF',
452
+ },
453
+ screenContainer: {
454
+ flex: 1,
455
+ padding: 20,
456
+ },
457
+ title: {
458
+ fontSize: 24,
459
+ fontWeight: '700',
460
+ color: '#fff',
461
+ marginBottom: 8,
462
+ },
463
+ subtitle: {
464
+ fontSize: 14,
465
+ color: '#888',
466
+ marginBottom: 24,
467
+ },
468
+ pinInput: {
469
+ backgroundColor: '#1a1a3e',
470
+ color: '#fff',
471
+ fontSize: 24,
472
+ textAlign: 'center',
473
+ letterSpacing: 8,
474
+ padding: 16,
475
+ borderRadius: 12,
476
+ marginBottom: 16,
477
+ },
478
+ errorText: {
479
+ color: '#ef4444',
480
+ fontSize: 14,
481
+ marginBottom: 16,
482
+ textAlign: 'center',
483
+ },
484
+ button: {
485
+ backgroundColor: '#007AFF',
486
+ paddingVertical: 14,
487
+ borderRadius: 10,
488
+ alignItems: 'center',
489
+ },
490
+ buttonDisabled: {
491
+ opacity: 0.5,
492
+ },
493
+ buttonText: {
494
+ color: '#fff',
495
+ fontSize: 16,
496
+ fontWeight: '600',
497
+ },
498
+ card: {
499
+ backgroundColor: '#1a1a3e',
500
+ borderRadius: 12,
501
+ padding: 16,
502
+ marginBottom: 16,
503
+ },
504
+ cardTitle: {
505
+ fontSize: 16,
506
+ fontWeight: '600',
507
+ color: '#fff',
508
+ marginBottom: 12,
509
+ },
510
+ row: {
511
+ flexDirection: 'row',
512
+ justifyContent: 'space-between',
513
+ alignItems: 'center',
514
+ paddingVertical: 6,
515
+ },
516
+ label: {
517
+ color: '#888',
518
+ fontSize: 14,
519
+ },
520
+ value: {
521
+ color: '#fff',
522
+ fontSize: 14,
523
+ fontFamily: 'Courier',
524
+ },
525
+ envBadge: {
526
+ backgroundColor: '#007AFF',
527
+ paddingHorizontal: 12,
528
+ paddingVertical: 4,
529
+ borderRadius: 6,
530
+ },
531
+ envBadgeText: {
532
+ color: '#fff',
533
+ fontSize: 12,
534
+ fontWeight: '600',
535
+ },
536
+ actionsRow: {
537
+ flexDirection: 'row',
538
+ gap: 8,
539
+ },
540
+ actionButton: {
541
+ backgroundColor: '#007AFF',
542
+ paddingHorizontal: 16,
543
+ paddingVertical: 10,
544
+ borderRadius: 8,
545
+ flex: 1,
546
+ alignItems: 'center',
547
+ },
548
+ dangerButton: {
549
+ backgroundColor: '#ef4444',
550
+ },
551
+ successButton: {
552
+ backgroundColor: '#22c55e',
553
+ },
554
+ actionButtonText: {
555
+ color: '#fff',
556
+ fontSize: 14,
557
+ fontWeight: '600',
558
+ },
559
+ bucketItem: {
560
+ flexDirection: 'row',
561
+ justifyContent: 'space-between',
562
+ alignItems: 'center',
563
+ paddingVertical: 12,
564
+ borderBottomWidth: 1,
565
+ borderBottomColor: '#252550',
566
+ },
567
+ bucketName: {
568
+ color: '#fff',
569
+ fontSize: 15,
570
+ fontWeight: '500',
571
+ },
572
+ bucketMeta: {
573
+ color: '#666',
574
+ fontSize: 12,
575
+ marginTop: 2,
576
+ },
577
+ chevron: {
578
+ color: '#666',
579
+ fontSize: 20,
580
+ },
581
+ emptyText: {
582
+ color: '#666',
583
+ textAlign: 'center',
584
+ paddingVertical: 20,
585
+ },
586
+ backButton: {
587
+ marginBottom: 12,
588
+ },
589
+ backText: {
590
+ color: '#007AFF',
591
+ fontSize: 16,
592
+ },
593
+ bundleCard: {
594
+ backgroundColor: '#1a1a3e',
595
+ borderRadius: 10,
596
+ padding: 14,
597
+ marginBottom: 10,
598
+ },
599
+ bundleHeader: {
600
+ flexDirection: 'row',
601
+ justifyContent: 'space-between',
602
+ alignItems: 'center',
603
+ marginBottom: 4,
604
+ },
605
+ bundleVersion: {
606
+ color: '#fff',
607
+ fontSize: 16,
608
+ fontWeight: '600',
609
+ },
610
+ statusBadge: {
611
+ paddingHorizontal: 8,
612
+ paddingVertical: 2,
613
+ borderRadius: 4,
614
+ },
615
+ releasedBadge: {
616
+ backgroundColor: '#22c55e33',
617
+ },
618
+ readyBadge: {
619
+ backgroundColor: '#f59e0b33',
620
+ },
621
+ statusBadgeText: {
622
+ fontSize: 11,
623
+ fontWeight: '600',
624
+ color: '#fff',
625
+ },
626
+ bundlePlatform: {
627
+ color: '#888',
628
+ fontSize: 12,
629
+ marginBottom: 4,
630
+ },
631
+ bundleNote: {
632
+ color: '#aaa',
633
+ fontSize: 13,
634
+ marginBottom: 8,
635
+ },
636
+ bundleFooter: {
637
+ flexDirection: 'row',
638
+ justifyContent: 'space-between',
639
+ alignItems: 'center',
640
+ marginTop: 8,
641
+ },
642
+ bundleSize: {
643
+ color: '#666',
644
+ fontSize: 12,
645
+ },
646
+ downloadButton: {
647
+ backgroundColor: '#007AFF',
648
+ paddingHorizontal: 16,
649
+ paddingVertical: 8,
650
+ borderRadius: 6,
651
+ },
652
+ downloadButtonText: {
653
+ color: '#fff',
654
+ fontSize: 13,
655
+ fontWeight: '600',
656
+ },
657
+ });
@@ -0,0 +1,24 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { UseSwiftPatchModalReturn } from '../types';
3
+
4
+ /**
5
+ * Hook for controlling the SwiftPatch in-app dashboard modal.
6
+ * Use this to show/hide the modal for bundle management.
7
+ */
8
+ export function useSwiftPatchModal(): UseSwiftPatchModalReturn {
9
+ const [isModalVisible, setIsModalVisible] = useState(false);
10
+
11
+ const showModal = useCallback(() => {
12
+ setIsModalVisible(true);
13
+ }, []);
14
+
15
+ const hideModal = useCallback(() => {
16
+ setIsModalVisible(false);
17
+ }, []);
18
+
19
+ return {
20
+ showModal,
21
+ hideModal,
22
+ isModalVisible,
23
+ };
24
+ }