@appspacer/react-native 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/LICENSE +21 -0
- package/README.md +108 -0
- package/android/build.gradle +28 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/appspacer/AppSpacerCrashHandler.java +128 -0
- package/android/src/main/java/com/appspacer/AppSpacerModule.java +731 -0
- package/android/src/main/java/com/appspacer/AppSpacerPackage.java +29 -0
- package/dist/AppSpacer.d.ts +116 -0
- package/dist/AppSpacer.js +546 -0
- package/dist/CrashReporter.d.ts +1 -0
- package/dist/CrashReporter.js +41 -0
- package/dist/NativeAppSpacer.d.ts +34 -0
- package/dist/NativeAppSpacer.js +2 -0
- package/dist/assetResolver.d.ts +24 -0
- package/dist/assetResolver.js +130 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/native.d.ts +88 -0
- package/dist/native.js +25 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +50 -0
- package/dist/useAppSpacerUpdate.d.ts +31 -0
- package/dist/useAppSpacerUpdate.js +81 -0
- package/dist/withAppSpacer.d.ts +4 -0
- package/dist/withAppSpacer.js +487 -0
- package/ios/AppSpacerCrashHandler.h +12 -0
- package/ios/AppSpacerCrashHandler.m +90 -0
- package/ios/AppSpacerModule.h +12 -0
- package/ios/AppSpacerModule.mm +545 -0
- package/package.json +51 -0
- package/react-native-appspacer.podspec +17 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { AppSpacer } from "./AppSpacer";
|
|
3
|
+
import { UpdateStatus } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Production-grade React hook for AppSpacer OTA updates.
|
|
6
|
+
*/
|
|
7
|
+
export function useAppSpacerUpdate() {
|
|
8
|
+
const [status, setStatus] = useState(AppSpacer.getStatus());
|
|
9
|
+
const [update, setUpdate] = useState(null);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const unsubscribeStatus = AppSpacer.onStatusChange((newStatus, detail) => {
|
|
14
|
+
setStatus(newStatus);
|
|
15
|
+
if (newStatus === UpdateStatus.ERROR) {
|
|
16
|
+
setError(detail ?? "Unknown error");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
setError(null);
|
|
20
|
+
}
|
|
21
|
+
// Reset progress when not downloading
|
|
22
|
+
if (newStatus !== UpdateStatus.DOWNLOADING) {
|
|
23
|
+
setDownloadProgress(0);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const unsubscribeProgress = AppSpacer.onDownloadProgress((progress) => {
|
|
27
|
+
setDownloadProgress(progress);
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
unsubscribeStatus();
|
|
31
|
+
unsubscribeProgress();
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
const checkForUpdate = useCallback(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const result = await AppSpacer.checkForUpdate();
|
|
37
|
+
setUpdate(result);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
setError(err.message);
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
const downloadUpdate = useCallback(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await AppSpacer.downloadUpdate();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
setError(err.message);
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
const restartApp = useCallback(() => {
|
|
52
|
+
AppSpacer.restartApp();
|
|
53
|
+
}, []);
|
|
54
|
+
const sync = useCallback(async (options) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await AppSpacer.sync(options);
|
|
57
|
+
setUpdate(result);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
setError(err.message);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
const clearOldUpdates = useCallback(async () => {
|
|
64
|
+
return AppSpacer.clearOldUpdates();
|
|
65
|
+
}, []);
|
|
66
|
+
const getUpdateMetadata = useCallback(async () => {
|
|
67
|
+
return AppSpacer.getUpdateMetadata();
|
|
68
|
+
}, []);
|
|
69
|
+
return {
|
|
70
|
+
status,
|
|
71
|
+
update,
|
|
72
|
+
error,
|
|
73
|
+
downloadProgress,
|
|
74
|
+
checkForUpdate,
|
|
75
|
+
downloadUpdate,
|
|
76
|
+
restartApp,
|
|
77
|
+
sync,
|
|
78
|
+
clearOldUpdates,
|
|
79
|
+
getUpdateMetadata,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AppSpacerConfig, SyncOptions } from "./types";
|
|
3
|
+
export type AppSpacerOptions = AppSpacerConfig & SyncOptions;
|
|
4
|
+
export declare function withAppSpacer(options: AppSpacerOptions): <P extends object>(WrappedComponent: React.ComponentType<P>) => React.ComponentType<P>;
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
3
|
+
import { AppState, Modal, View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions, ActivityIndicator,
|
|
4
|
+
// eslint-disable-next-line deprecation/deprecation
|
|
5
|
+
SafeAreaView, } from "react-native";
|
|
6
|
+
import { AppSpacer } from "./AppSpacer";
|
|
7
|
+
import { UpdateStatus, InstallMode, UpdateUIType } from "./types";
|
|
8
|
+
const { width } = Dimensions.get("window");
|
|
9
|
+
export function withAppSpacer(options) {
|
|
10
|
+
const selectedUI = options.updateUI || UpdateUIType.PREMIUM_DARK;
|
|
11
|
+
return function (WrappedComponent) {
|
|
12
|
+
return function AppSpacerWrapper(props) {
|
|
13
|
+
const [showModal, setShowModal] = useState(false);
|
|
14
|
+
const [update, setUpdate] = useState(null);
|
|
15
|
+
const [status, setStatus] = useState(UpdateStatus.IDLE);
|
|
16
|
+
const [progress, setProgress] = useState(0);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
AppSpacer.init({
|
|
19
|
+
deploymentKey: options.deploymentKey,
|
|
20
|
+
appVersion: options.appVersion,
|
|
21
|
+
checkInterval: options.checkInterval,
|
|
22
|
+
enableCrashReporting: options.enableCrashReporting,
|
|
23
|
+
});
|
|
24
|
+
return () => AppSpacer.destroy();
|
|
25
|
+
}, []);
|
|
26
|
+
// ✅ Use a ref so AppState listener always reads current status
|
|
27
|
+
// without needing to be re-registered on every status change
|
|
28
|
+
const statusRef = useRef(UpdateStatus.IDLE);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
statusRef.current = status;
|
|
31
|
+
}, [status]);
|
|
32
|
+
// ✅ Resolve the correct install mode given a specific update
|
|
33
|
+
const resolveInstallMode = useCallback((currentUpdate) => {
|
|
34
|
+
if (currentUpdate.mandatory) {
|
|
35
|
+
return (options.mandatoryInstallMode ??
|
|
36
|
+
options.installMode ??
|
|
37
|
+
InstallMode.IMMEDIATE);
|
|
38
|
+
}
|
|
39
|
+
return options.installMode ?? InstallMode.ON_NEXT_RESTART;
|
|
40
|
+
}, []);
|
|
41
|
+
// ✅ Accept currentUpdate directly — avoids stale state closure
|
|
42
|
+
const handleSilentDownload = useCallback(async (currentUpdate) => {
|
|
43
|
+
try {
|
|
44
|
+
await AppSpacer.downloadUpdate();
|
|
45
|
+
const mode = resolveInstallMode(currentUpdate);
|
|
46
|
+
if (mode === InstallMode.IMMEDIATE) {
|
|
47
|
+
AppSpacer.restartApp();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Silent background download failures are non-fatal
|
|
52
|
+
}
|
|
53
|
+
}, [resolveInstallMode]);
|
|
54
|
+
const runSync = useCallback(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await AppSpacer.checkForUpdate();
|
|
57
|
+
if (!result.updateAvailable)
|
|
58
|
+
return;
|
|
59
|
+
setUpdate(result);
|
|
60
|
+
if (options.updateDialog) {
|
|
61
|
+
setShowModal(true);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// ✅ Pass result directly — no stale state risk
|
|
65
|
+
await handleSilentDownload(result);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Check failures are non-fatal
|
|
70
|
+
}
|
|
71
|
+
}, [handleSilentDownload]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const unsubscribeStatus = AppSpacer.onStatusChange((s) => setStatus(s));
|
|
74
|
+
const unsubscribeProgress = AppSpacer.onDownloadProgress((p) => setProgress(p));
|
|
75
|
+
runSync();
|
|
76
|
+
const subscription = AppState.addEventListener("change", (nextState) => {
|
|
77
|
+
// ✅ Guard: don't re-trigger check if already downloading/installing
|
|
78
|
+
if (nextState === "active" &&
|
|
79
|
+
statusRef.current === UpdateStatus.IDLE) {
|
|
80
|
+
runSync();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return () => {
|
|
84
|
+
subscription.remove();
|
|
85
|
+
unsubscribeStatus();
|
|
86
|
+
unsubscribeProgress();
|
|
87
|
+
};
|
|
88
|
+
}, [runSync]);
|
|
89
|
+
// ✅ Respects installMode — no longer always calls restartApp()
|
|
90
|
+
const confirmInstall = useCallback(async () => {
|
|
91
|
+
try {
|
|
92
|
+
await AppSpacer.downloadUpdate();
|
|
93
|
+
const mode = resolveInstallMode(update ?? { updateAvailable: true });
|
|
94
|
+
if (mode === InstallMode.IMMEDIATE) {
|
|
95
|
+
AppSpacer.restartApp();
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// ON_NEXT_RESTART or ON_NEXT_RESUME — close modal, update applies on next launch
|
|
99
|
+
setShowModal(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
setShowModal(false);
|
|
104
|
+
}
|
|
105
|
+
}, [update, resolveInstallMode]);
|
|
106
|
+
const ignoreInstall = useCallback(() => setShowModal(false), []);
|
|
107
|
+
const isProcessing = status === UpdateStatus.DOWNLOADING ||
|
|
108
|
+
status === UpdateStatus.INSTALLING;
|
|
109
|
+
const renderUI = () => {
|
|
110
|
+
switch (selectedUI) {
|
|
111
|
+
// ─────────────────────────────────────────────────────────────
|
|
112
|
+
case UpdateUIType.GLASS:
|
|
113
|
+
return (_jsxs(View, { style: [
|
|
114
|
+
styles.glassCard,
|
|
115
|
+
{ backgroundColor: "rgba(255,255,255,0.15)" },
|
|
116
|
+
], children: [_jsx(Text, { style: styles.glassTitle, children: update?.mandatory ? "Urgent Patch" : "App Upgrade" }), _jsx(Text, { style: styles.glassDesc, children: update?.description || "Stability and performance improvements." }), isProcessing ? (_jsx(ActivityIndicator, { color: "#FFF", size: "large" })) : (_jsxs(View, { style: styles.glassActions, children: [!update?.mandatory && (_jsx(TouchableOpacity, { onPress: ignoreInstall, style: styles.glassBtnSecondary, children: _jsx(Text, { style: styles.glassBtnTextSecondary, children: "Later" }) })), _jsx(TouchableOpacity, { onPress: confirmInstall, style: styles.glassBtnPrimary, children: _jsx(Text, { style: styles.glassBtnTextPrimary, children: "Update Now" }) })] }))] }));
|
|
117
|
+
// ─────────────────────────────────────────────────────────────
|
|
118
|
+
case UpdateUIType.MINIMAL:
|
|
119
|
+
return (_jsxs(View, { style: styles.minimalCard, children: [_jsxs(View, { style: styles.minimalHeader, children: [_jsx(Text, { style: styles.minimalTitle, children: "Update Required" }), update?.label ? (_jsxs(Text, { style: styles.minimalVersion, children: ["v", update.label] })) : null] }), _jsx(Text, { style: styles.minimalDesc, children: update?.description || "Release notes not provided." }), isProcessing ? (
|
|
120
|
+
// ✅ Added track container so bar is visible at 0%
|
|
121
|
+
_jsx(View, { style: styles.minimalProgressTrack, children: _jsx(View, { style: [
|
|
122
|
+
styles.minimalBar,
|
|
123
|
+
{ width: `${Math.round(progress * 100)}%` },
|
|
124
|
+
] }) })) : (_jsxs(View, { style: styles.minimalFooter, children: [!update?.mandatory && (_jsx(TouchableOpacity, { onPress: ignoreInstall, children: _jsx(Text, { style: styles.minimalLink, children: "Postpone" }) })), _jsx(TouchableOpacity, { onPress: confirmInstall, style: styles.minimalBtn, children: _jsx(Text, { style: styles.minimalBtnText, children: "Update" }) })] }))] }));
|
|
125
|
+
// ─────────────────────────────────────────────────────────────
|
|
126
|
+
case UpdateUIType.BOTTOM_SHEET:
|
|
127
|
+
return (
|
|
128
|
+
// ✅ Sheet manages its own bottom alignment — not relying on overlay
|
|
129
|
+
_jsx(View, { style: styles.sheetWrapper, children: _jsxs(View, { style: styles.sheet, children: [_jsx(View, { style: styles.sheetHandle }), _jsx(Text, { style: styles.sheetTitle, children: "New Version Available" }), _jsx(Text, { style: styles.sheetDesc, children: "Get the latest features and bug fixes. The app will restart after install." }), isProcessing ? (_jsxs(View, { style: styles.sheetLoading, children: [_jsx(ActivityIndicator, { color: "#6366F1" }), _jsxs(Text, { style: styles.sheetLoadingText, children: ["Downloading... ", Math.round(progress * 100), "%"] })] })) : (_jsxs(View, { style: styles.sheetActions, children: [_jsx(TouchableOpacity, { onPress: confirmInstall, style: styles.sheetBtnPrimary, children: _jsx(Text, { style: styles.sheetBtnTextPrimary, children: "Install Update" }) }), !update?.mandatory && (_jsx(TouchableOpacity, { onPress: ignoreInstall, style: styles.sheetBtnSecondary, children: _jsx(Text, { style: styles.sheetBtnTextSecondary, children: "Not Now" }) }))] }))] }) }));
|
|
130
|
+
// ─────────────────────────────────────────────────────────────
|
|
131
|
+
case UpdateUIType.FULL_SCREEN:
|
|
132
|
+
return (_jsxs(SafeAreaView, { style: styles.fullScreen, children: [_jsx(View, { style: styles.fullScreenGlow }), _jsx(Text, { style: styles.fullScreenEmoji, children: "\u2728" }), _jsx(Text, { style: styles.fullScreenTitle, children: "Time to Expand" }), _jsx(Text, { style: styles.fullScreenDesc, children: "A new build is ready. We've optimized the core engine and added new capabilities." }), _jsx(View, { style: styles.fullScreenFooter, children: isProcessing ? (
|
|
133
|
+
// ✅ Added track behind the fill bar
|
|
134
|
+
_jsxs(View, { style: styles.fullScreenProgressWrapper, children: [_jsx(View, { style: styles.fullScreenProgressTrack, children: _jsx(View, { style: [
|
|
135
|
+
styles.fullScreenBar,
|
|
136
|
+
{ width: `${Math.round(progress * 100)}%` },
|
|
137
|
+
] }) }), _jsxs(Text, { style: styles.fullScreenProgressText, children: [Math.round(progress * 100), "% Complete"] })] })) : (_jsxs(_Fragment, { children: [_jsx(TouchableOpacity, { onPress: confirmInstall, style: styles.fullScreenBtn, children: _jsx(Text, { style: styles.fullScreenBtnText, children: "Deploy Update" }) }), !update?.mandatory && (_jsx(TouchableOpacity, { onPress: ignoreInstall, style: styles.fullScreenMinor, children: _jsx(Text, { style: styles.fullScreenMinorText, children: "Skip for now" }) }))] })) })] }));
|
|
138
|
+
// ─────────────────────────────────────────────────────────────
|
|
139
|
+
case UpdateUIType.PREMIUM_DARK:
|
|
140
|
+
default:
|
|
141
|
+
return (_jsxs(View, { style: styles.card, children: [_jsx(View, { style: styles.glow }), _jsxs(View, { style: styles.content, children: [_jsx(View, { style: styles.iconBox, children: _jsx(Text, { style: styles.icon, children: "\u26A1" }) }), _jsx(Text, { style: styles.title, children: update?.mandatory ? "Mandatory Update" : "Update Available" }), update?.label ? (_jsxs(Text, { style: styles.version, children: ["AppSpacer v", update.label] })) : null, _jsx(View, { style: styles.descBox, children: _jsx(Text, { style: styles.description, children: update?.description || "Improvements and patches." }) }), isProcessing ? (_jsxs(View, { style: styles.progressContainer, children: [_jsx(View, { style: styles.progressBarBg, children: _jsx(View, { style: [
|
|
142
|
+
styles.progressBarFill,
|
|
143
|
+
{ width: `${Math.round(progress * 100)}%` },
|
|
144
|
+
] }) }), _jsxs(Text, { style: styles.progressText, children: [status === UpdateStatus.DOWNLOADING
|
|
145
|
+
? "Downloading..."
|
|
146
|
+
: "Applying...", " ", Math.round(progress * 100), "%"] })] })) : (_jsxs(View, { style: styles.footer, children: [!update?.mandatory && (_jsx(TouchableOpacity, { style: styles.ignoreBtn, onPress: ignoreInstall, children: _jsx(Text, { style: styles.ignoreText, children: "Later" }) })), _jsx(TouchableOpacity, { style: styles.installBtn, onPress: confirmInstall, children: _jsx(Text, { style: styles.installText, children: "Install Now" }) })] }))] })] }));
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
// ✅ Overlay justifyContent is conditional per UI type
|
|
150
|
+
const overlayStyle = [
|
|
151
|
+
styles.overlay,
|
|
152
|
+
selectedUI === UpdateUIType.FULL_SCREEN && styles.overlayFullScreen,
|
|
153
|
+
selectedUI === UpdateUIType.BOTTOM_SHEET && styles.overlayBottomSheet,
|
|
154
|
+
];
|
|
155
|
+
return (_jsxs(View, { style: { flex: 1 }, children: [_jsx(WrappedComponent, { ...props }), _jsx(Modal, { visible: showModal, transparent: true, animationType: selectedUI === UpdateUIType.BOTTOM_SHEET ? "slide" : "fade", statusBarTranslucent: true, children: _jsx(View, { style: overlayStyle, children: renderUI() }) })] }));
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
160
|
+
const styles = StyleSheet.create({
|
|
161
|
+
// OVERLAY — base (centered)
|
|
162
|
+
overlay: {
|
|
163
|
+
flex: 1,
|
|
164
|
+
backgroundColor: "rgba(0,0,0,0.85)",
|
|
165
|
+
justifyContent: "center",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
},
|
|
168
|
+
// ✅ Full-screen variant — no backdrop, fills screen
|
|
169
|
+
overlayFullScreen: {
|
|
170
|
+
backgroundColor: "#080B14",
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
},
|
|
174
|
+
// ✅ Bottom sheet variant — aligns to bottom, not center
|
|
175
|
+
overlayBottomSheet: {
|
|
176
|
+
justifyContent: "flex-end",
|
|
177
|
+
alignItems: "stretch",
|
|
178
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
179
|
+
},
|
|
180
|
+
// ── PREMIUM_DARK ────────────────────────────────────────────────────────
|
|
181
|
+
card: {
|
|
182
|
+
width: width * 0.85,
|
|
183
|
+
backgroundColor: "#161E2E",
|
|
184
|
+
borderRadius: 32,
|
|
185
|
+
borderWidth: 1,
|
|
186
|
+
borderColor: "rgba(255,255,255,0.1)",
|
|
187
|
+
overflow: "hidden",
|
|
188
|
+
paddingTop: 12,
|
|
189
|
+
},
|
|
190
|
+
glow: {
|
|
191
|
+
position: "absolute",
|
|
192
|
+
top: -100,
|
|
193
|
+
left: "25%",
|
|
194
|
+
right: "25%",
|
|
195
|
+
height: 150,
|
|
196
|
+
backgroundColor: "#6366F1",
|
|
197
|
+
opacity: 0.1,
|
|
198
|
+
borderRadius: 100,
|
|
199
|
+
transform: [{ scaleX: 2 }],
|
|
200
|
+
},
|
|
201
|
+
content: { padding: 24, alignItems: "center" },
|
|
202
|
+
iconBox: {
|
|
203
|
+
width: 64,
|
|
204
|
+
height: 64,
|
|
205
|
+
borderRadius: 22,
|
|
206
|
+
backgroundColor: "rgba(99,102,241,0.15)",
|
|
207
|
+
justifyContent: "center",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
marginBottom: 20,
|
|
210
|
+
borderWidth: 1,
|
|
211
|
+
borderColor: "rgba(99,102,241,0.3)",
|
|
212
|
+
},
|
|
213
|
+
icon: { fontSize: 32 },
|
|
214
|
+
title: {
|
|
215
|
+
color: "#FFFFFF",
|
|
216
|
+
fontSize: 22,
|
|
217
|
+
fontWeight: "900",
|
|
218
|
+
textAlign: "center",
|
|
219
|
+
marginBottom: 6,
|
|
220
|
+
},
|
|
221
|
+
version: {
|
|
222
|
+
color: "#6366F1",
|
|
223
|
+
fontSize: 14,
|
|
224
|
+
fontWeight: "800",
|
|
225
|
+
marginBottom: 20,
|
|
226
|
+
backgroundColor: "rgba(99,102,241,0.1)",
|
|
227
|
+
paddingHorizontal: 12,
|
|
228
|
+
paddingVertical: 4,
|
|
229
|
+
borderRadius: 12,
|
|
230
|
+
},
|
|
231
|
+
descBox: {
|
|
232
|
+
backgroundColor: "rgba(255,255,255,0.03)",
|
|
233
|
+
padding: 16,
|
|
234
|
+
borderRadius: 18,
|
|
235
|
+
width: "100%",
|
|
236
|
+
marginBottom: 32,
|
|
237
|
+
},
|
|
238
|
+
description: {
|
|
239
|
+
color: "#94A3B8",
|
|
240
|
+
fontSize: 14,
|
|
241
|
+
textAlign: "center",
|
|
242
|
+
lineHeight: 22,
|
|
243
|
+
},
|
|
244
|
+
footer: { flexDirection: "row", width: "100%", gap: 12 },
|
|
245
|
+
ignoreBtn: {
|
|
246
|
+
flex: 1,
|
|
247
|
+
height: 58,
|
|
248
|
+
borderRadius: 18,
|
|
249
|
+
justifyContent: "center",
|
|
250
|
+
alignItems: "center",
|
|
251
|
+
backgroundColor: "rgba(255,255,255,0.03)",
|
|
252
|
+
},
|
|
253
|
+
ignoreText: { color: "#94A3B8", fontSize: 16, fontWeight: "700" },
|
|
254
|
+
installBtn: {
|
|
255
|
+
flex: 2,
|
|
256
|
+
height: 58,
|
|
257
|
+
borderRadius: 18,
|
|
258
|
+
justifyContent: "center",
|
|
259
|
+
alignItems: "center",
|
|
260
|
+
backgroundColor: "#6366F1",
|
|
261
|
+
},
|
|
262
|
+
installText: { color: "#FFFFFF", fontSize: 16, fontWeight: "900" },
|
|
263
|
+
progressContainer: { width: "100%", alignItems: "center" },
|
|
264
|
+
progressBarBg: {
|
|
265
|
+
width: "100%",
|
|
266
|
+
height: 8,
|
|
267
|
+
backgroundColor: "rgba(255,255,255,0.05)",
|
|
268
|
+
borderRadius: 4,
|
|
269
|
+
overflow: "hidden",
|
|
270
|
+
marginBottom: 12,
|
|
271
|
+
},
|
|
272
|
+
progressBarFill: {
|
|
273
|
+
height: "100%",
|
|
274
|
+
backgroundColor: "#6366F1",
|
|
275
|
+
borderRadius: 4,
|
|
276
|
+
},
|
|
277
|
+
progressText: {
|
|
278
|
+
color: "#6366F1",
|
|
279
|
+
fontSize: 12,
|
|
280
|
+
fontWeight: "800",
|
|
281
|
+
textTransform: "uppercase",
|
|
282
|
+
letterSpacing: 1,
|
|
283
|
+
},
|
|
284
|
+
// ── GLASS ───────────────────────────────────────────────────────────────
|
|
285
|
+
glassCard: {
|
|
286
|
+
width: width * 0.88,
|
|
287
|
+
borderRadius: 36,
|
|
288
|
+
padding: 32,
|
|
289
|
+
borderWidth: 1,
|
|
290
|
+
borderColor: "rgba(255,255,255,0.2)",
|
|
291
|
+
alignItems: "center",
|
|
292
|
+
},
|
|
293
|
+
glassTitle: {
|
|
294
|
+
color: "#FFF",
|
|
295
|
+
fontSize: 24,
|
|
296
|
+
fontWeight: "900",
|
|
297
|
+
marginBottom: 16,
|
|
298
|
+
},
|
|
299
|
+
glassDesc: {
|
|
300
|
+
color: "rgba(255,255,255,0.7)",
|
|
301
|
+
fontSize: 16,
|
|
302
|
+
textAlign: "center",
|
|
303
|
+
marginBottom: 32,
|
|
304
|
+
},
|
|
305
|
+
glassActions: { width: "100%", gap: 12 },
|
|
306
|
+
glassBtnPrimary: {
|
|
307
|
+
backgroundColor: "#FFF",
|
|
308
|
+
height: 60,
|
|
309
|
+
borderRadius: 20,
|
|
310
|
+
alignItems: "center",
|
|
311
|
+
justifyContent: "center",
|
|
312
|
+
},
|
|
313
|
+
glassBtnSecondary: {
|
|
314
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
315
|
+
height: 60,
|
|
316
|
+
borderRadius: 20,
|
|
317
|
+
alignItems: "center",
|
|
318
|
+
justifyContent: "center",
|
|
319
|
+
},
|
|
320
|
+
// ✅ Split into two styles — primary is dark text on white, secondary is white text
|
|
321
|
+
glassBtnTextPrimary: { color: "#000", fontSize: 16, fontWeight: "800" },
|
|
322
|
+
glassBtnTextSecondary: { color: "#FFF", fontSize: 16, fontWeight: "700" },
|
|
323
|
+
// ── MINIMAL ─────────────────────────────────────────────────────────────
|
|
324
|
+
minimalCard: {
|
|
325
|
+
width: width * 0.9,
|
|
326
|
+
backgroundColor: "#FFF",
|
|
327
|
+
borderRadius: 24,
|
|
328
|
+
padding: 24,
|
|
329
|
+
},
|
|
330
|
+
minimalHeader: {
|
|
331
|
+
flexDirection: "row",
|
|
332
|
+
justifyContent: "space-between",
|
|
333
|
+
alignItems: "center",
|
|
334
|
+
marginBottom: 12,
|
|
335
|
+
},
|
|
336
|
+
minimalTitle: { fontSize: 18, fontWeight: "800", color: "#111" },
|
|
337
|
+
minimalVersion: {
|
|
338
|
+
fontSize: 12,
|
|
339
|
+
fontWeight: "700",
|
|
340
|
+
color: "#6366F1",
|
|
341
|
+
backgroundColor: "#EEF2FF",
|
|
342
|
+
paddingHorizontal: 8,
|
|
343
|
+
paddingVertical: 2,
|
|
344
|
+
borderRadius: 6,
|
|
345
|
+
},
|
|
346
|
+
minimalDesc: {
|
|
347
|
+
fontSize: 14,
|
|
348
|
+
color: "#666",
|
|
349
|
+
lineHeight: 20,
|
|
350
|
+
marginBottom: 24,
|
|
351
|
+
},
|
|
352
|
+
minimalFooter: {
|
|
353
|
+
flexDirection: "row",
|
|
354
|
+
justifyContent: "space-between",
|
|
355
|
+
alignItems: "center",
|
|
356
|
+
},
|
|
357
|
+
minimalLink: { color: "#999", fontSize: 14, fontWeight: "600" },
|
|
358
|
+
minimalBtn: {
|
|
359
|
+
backgroundColor: "#111",
|
|
360
|
+
paddingHorizontal: 20,
|
|
361
|
+
paddingVertical: 12,
|
|
362
|
+
borderRadius: 12,
|
|
363
|
+
},
|
|
364
|
+
minimalBtnText: { color: "#FFF", fontWeight: "800" },
|
|
365
|
+
// ✅ Track added so fill is visible at 0%
|
|
366
|
+
minimalProgressTrack: {
|
|
367
|
+
height: 4,
|
|
368
|
+
backgroundColor: "#F3F4F6",
|
|
369
|
+
borderRadius: 2,
|
|
370
|
+
overflow: "hidden",
|
|
371
|
+
marginBottom: 16,
|
|
372
|
+
},
|
|
373
|
+
minimalBar: { height: "100%", backgroundColor: "#6366F1", borderRadius: 2 },
|
|
374
|
+
// ── BOTTOM SHEET ────────────────────────────────────────────────────────
|
|
375
|
+
// ✅ Wrapper fills width and pushes sheet to bottom via overlay's flex-end
|
|
376
|
+
sheetWrapper: { width: "100%" },
|
|
377
|
+
sheet: {
|
|
378
|
+
backgroundColor: "#FFF",
|
|
379
|
+
borderTopLeftRadius: 32,
|
|
380
|
+
borderTopRightRadius: 32,
|
|
381
|
+
padding: 32,
|
|
382
|
+
paddingBottom: Platform.OS === "ios" ? 48 : 32,
|
|
383
|
+
},
|
|
384
|
+
sheetHandle: {
|
|
385
|
+
width: 40,
|
|
386
|
+
height: 5,
|
|
387
|
+
backgroundColor: "#E5E7EB",
|
|
388
|
+
borderRadius: 3,
|
|
389
|
+
alignSelf: "center",
|
|
390
|
+
marginBottom: 24,
|
|
391
|
+
},
|
|
392
|
+
sheetTitle: {
|
|
393
|
+
fontSize: 22,
|
|
394
|
+
fontWeight: "900",
|
|
395
|
+
color: "#111",
|
|
396
|
+
marginBottom: 12,
|
|
397
|
+
},
|
|
398
|
+
sheetDesc: {
|
|
399
|
+
fontSize: 15,
|
|
400
|
+
color: "#4B5563",
|
|
401
|
+
lineHeight: 22,
|
|
402
|
+
marginBottom: 32,
|
|
403
|
+
},
|
|
404
|
+
sheetActions: { gap: 12 },
|
|
405
|
+
sheetBtnPrimary: {
|
|
406
|
+
backgroundColor: "#6366F1",
|
|
407
|
+
height: 60,
|
|
408
|
+
borderRadius: 20,
|
|
409
|
+
alignItems: "center",
|
|
410
|
+
justifyContent: "center",
|
|
411
|
+
},
|
|
412
|
+
sheetBtnSecondary: {
|
|
413
|
+
backgroundColor: "#F3F4F6",
|
|
414
|
+
height: 60,
|
|
415
|
+
borderRadius: 20,
|
|
416
|
+
alignItems: "center",
|
|
417
|
+
justifyContent: "center",
|
|
418
|
+
},
|
|
419
|
+
sheetBtnTextPrimary: { color: "#FFF", fontSize: 16, fontWeight: "800" },
|
|
420
|
+
sheetBtnTextSecondary: {
|
|
421
|
+
color: "#4B5563",
|
|
422
|
+
fontSize: 16,
|
|
423
|
+
fontWeight: "700",
|
|
424
|
+
},
|
|
425
|
+
sheetLoading: { alignItems: "center", gap: 12 },
|
|
426
|
+
sheetLoadingText: { color: "#6366F1", fontWeight: "800" },
|
|
427
|
+
// ── FULL SCREEN ─────────────────────────────────────────────────────────
|
|
428
|
+
fullScreen: {
|
|
429
|
+
flex: 1,
|
|
430
|
+
width: "100%",
|
|
431
|
+
backgroundColor: "#080B14",
|
|
432
|
+
alignItems: "center",
|
|
433
|
+
justifyContent: "center",
|
|
434
|
+
padding: 40,
|
|
435
|
+
},
|
|
436
|
+
fullScreenGlow: {
|
|
437
|
+
position: "absolute",
|
|
438
|
+
width: width * 1.5,
|
|
439
|
+
height: width * 1.5,
|
|
440
|
+
backgroundColor: "#6366F1",
|
|
441
|
+
opacity: 0.05,
|
|
442
|
+
borderRadius: width,
|
|
443
|
+
},
|
|
444
|
+
fullScreenEmoji: { fontSize: 64, marginBottom: 40 },
|
|
445
|
+
fullScreenTitle: {
|
|
446
|
+
color: "#FFF",
|
|
447
|
+
fontSize: 32,
|
|
448
|
+
fontWeight: "900",
|
|
449
|
+
textAlign: "center",
|
|
450
|
+
marginBottom: 20,
|
|
451
|
+
},
|
|
452
|
+
fullScreenDesc: {
|
|
453
|
+
color: "#94A3B8",
|
|
454
|
+
fontSize: 17,
|
|
455
|
+
textAlign: "center",
|
|
456
|
+
lineHeight: 26,
|
|
457
|
+
marginBottom: 60,
|
|
458
|
+
},
|
|
459
|
+
fullScreenFooter: { width: "100%", alignItems: "center" },
|
|
460
|
+
fullScreenBtn: {
|
|
461
|
+
backgroundColor: "#6366F1",
|
|
462
|
+
width: "100%",
|
|
463
|
+
height: 64,
|
|
464
|
+
borderRadius: 22,
|
|
465
|
+
alignItems: "center",
|
|
466
|
+
justifyContent: "center",
|
|
467
|
+
},
|
|
468
|
+
fullScreenBtnText: { color: "#FFF", fontSize: 18, fontWeight: "900" },
|
|
469
|
+
fullScreenMinor: { marginTop: 24 },
|
|
470
|
+
fullScreenMinorText: { color: "#475569", fontSize: 16, fontWeight: "600" },
|
|
471
|
+
// ✅ Proper progress track + fill structure
|
|
472
|
+
fullScreenProgressWrapper: { width: "100%", alignItems: "center" },
|
|
473
|
+
fullScreenProgressTrack: {
|
|
474
|
+
width: "100%",
|
|
475
|
+
height: 6,
|
|
476
|
+
backgroundColor: "rgba(255,255,255,0.08)",
|
|
477
|
+
borderRadius: 3,
|
|
478
|
+
overflow: "hidden",
|
|
479
|
+
marginBottom: 16,
|
|
480
|
+
},
|
|
481
|
+
fullScreenBar: {
|
|
482
|
+
height: "100%",
|
|
483
|
+
backgroundColor: "#6366F1",
|
|
484
|
+
borderRadius: 3,
|
|
485
|
+
},
|
|
486
|
+
fullScreenProgressText: { color: "#6366F1", fontWeight: "900" },
|
|
487
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#import "AppSpacerCrashHandler.h"
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
static NSUncaughtExceptionHandler *defaultUncaughtExceptionHandler;
|
|
5
|
+
static BOOL isInitialized = NO;
|
|
6
|
+
|
|
7
|
+
// Function to synchronously format and save the crash
|
|
8
|
+
static void SaveNativeCrashToDisk(NSString *reason, NSArray *stackSymbols) {
|
|
9
|
+
NSString *crashId = [NSString stringWithFormat:@"native-%lld-%05d", (long long)([[NSDate date] timeIntervalSince1970] * 1000), arc4random_uniform(100000)];
|
|
10
|
+
|
|
11
|
+
NSMutableDictionary *crashData = [NSMutableDictionary dictionary];
|
|
12
|
+
crashData[@"id"] = crashId;
|
|
13
|
+
crashData[@"type"] = @"native";
|
|
14
|
+
|
|
15
|
+
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
|
|
16
|
+
payload[@"message"] = reason;
|
|
17
|
+
payload[@"stack"] = [stackSymbols componentsJoinedByString:@"\n"];
|
|
18
|
+
payload[@"isFatal"] = @YES;
|
|
19
|
+
|
|
20
|
+
crashData[@"payload"] = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:payload options:0 error:nil] encoding:NSUTF8StringEncoding];
|
|
21
|
+
|
|
22
|
+
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
|
23
|
+
formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
|
|
24
|
+
formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
|
25
|
+
crashData[@"timestamp"] = [formatter stringFromDate:[NSDate date]];
|
|
26
|
+
|
|
27
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:crashData options:0 error:nil];
|
|
28
|
+
if (jsonData) {
|
|
29
|
+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
30
|
+
[AppSpacerCrashHandler saveCrashToDiskSyncWithId:crashId payload:jsonString];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exception Handler
|
|
35
|
+
static void AppSpacerUncaughtExceptionHandler(NSException *exception) {
|
|
36
|
+
NSString *reason = [NSString stringWithFormat:@"%@: %@", exception.name, exception.reason];
|
|
37
|
+
SaveNativeCrashToDisk(reason, [exception callStackSymbols]);
|
|
38
|
+
|
|
39
|
+
if (defaultUncaughtExceptionHandler != NULL) {
|
|
40
|
+
defaultUncaughtExceptionHandler(exception);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Signal Handler
|
|
45
|
+
static void AppSpacerSignalHandler(int signal) {
|
|
46
|
+
NSString *reason = [NSString stringWithFormat:@"Signal %d received", signal];
|
|
47
|
+
SaveNativeCrashToDisk(reason, [NSThread callStackSymbols]);
|
|
48
|
+
|
|
49
|
+
// We intentionally don't chain standard BSD signals here to avoid loops,
|
|
50
|
+
// the system will terminate the process anyway because we don't catch the signal
|
|
51
|
+
// a second time (SA_RESETHAND could be used natively, but for now we just exit)
|
|
52
|
+
exit(signal);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@implementation AppSpacerCrashHandler
|
|
56
|
+
|
|
57
|
+
+ (void)start {
|
|
58
|
+
if (isInitialized) return;
|
|
59
|
+
isInitialized = YES;
|
|
60
|
+
|
|
61
|
+
defaultUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
|
|
62
|
+
NSSetUncaughtExceptionHandler(&AppSpacerUncaughtExceptionHandler);
|
|
63
|
+
|
|
64
|
+
signal(SIGABRT, AppSpacerSignalHandler);
|
|
65
|
+
signal(SIGILL, AppSpacerSignalHandler);
|
|
66
|
+
signal(SIGSEGV, AppSpacerSignalHandler);
|
|
67
|
+
signal(SIGFPE, AppSpacerSignalHandler);
|
|
68
|
+
signal(SIGBUS, AppSpacerSignalHandler);
|
|
69
|
+
signal(SIGPIPE, AppSpacerSignalHandler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
+ (NSString *)crashDirectory {
|
|
73
|
+
NSString *cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
|
|
74
|
+
NSString *crashPath = [cachesDirectory stringByAppendingPathComponent:@"appspacer_crashes"];
|
|
75
|
+
|
|
76
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
77
|
+
if (![fm fileExistsAtPath:crashPath]) {
|
|
78
|
+
[fm createDirectoryAtPath:crashPath withIntermediateDirectories:YES attributes:nil error:nil];
|
|
79
|
+
}
|
|
80
|
+
return crashPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
+ (void)saveCrashToDiskSyncWithId:(NSString *)crashId payload:(NSString *)payload {
|
|
84
|
+
NSString *dir = [self crashDirectory];
|
|
85
|
+
NSString *filePath = [dir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.json", crashId]];
|
|
86
|
+
|
|
87
|
+
[payload writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@end
|