umpordez 1.3.2 → 1.4.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/.claude/settings.local.json +6 -1
- package/cli.mjs +1 -1
- package/create.mjs +179 -116
- package/package.json +1 -1
- package/template/CLAUDE.md +5 -4
- package/template/README.md +3 -3
- package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +11 -2
- package/template/mobile/{{PROJECT_SLUG}}/app.json +13 -31
- package/template/mobile/{{PROJECT_SLUG}}/package.json +22 -20
- package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +5 -1
- package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +11 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/pages/auth/AuthLanding.tsx +55 -50
- package/template-variables.mjs +127 -0
- package/update.mjs +52 -48
- package/template/mobile/{{PROJECT_SLUG}}/src/services/health/index.ts +0 -238
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health — unified surface over Apple Health (iOS, react-native-health)
|
|
3
|
-
* and Health Connect (Android, react-native-health-connect).
|
|
4
|
-
*
|
|
5
|
-
* Both libraries are lazy-loaded with try/require so Expo Go (which
|
|
6
|
-
* has neither linked) gets a no-op service instead of a hard crash.
|
|
7
|
-
*
|
|
8
|
-
* The module exposes a small, opinionated API:
|
|
9
|
-
* - requestPermissions — surface the platform's permissions UI
|
|
10
|
-
* - getStepsToday — read example
|
|
11
|
-
* - writeExerciseSession — write example: minutes-of-X workout
|
|
12
|
-
*
|
|
13
|
-
* Adapt to whatever data your app actually cares about. Mirror this
|
|
14
|
-
* shape: one method per "thing the app needs" rather than
|
|
15
|
-
* one-method-per-data-type, so a swap of platforms or libraries
|
|
16
|
-
* stays a contained change.
|
|
17
|
-
*
|
|
18
|
-
* All errors are swallowed by callers — health is best-effort.
|
|
19
|
-
* Returns null / 0 on failure rather than throwing.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { Platform } from 'react-native';
|
|
23
|
-
|
|
24
|
-
// ── iOS HealthKit (react-native-health) ──────────────────────────────
|
|
25
|
-
interface AppleHealthKit {
|
|
26
|
-
initHealthKit: (
|
|
27
|
-
options: unknown,
|
|
28
|
-
cb: (err: string | null) => void
|
|
29
|
-
) => void;
|
|
30
|
-
getStepCount: (
|
|
31
|
-
opts: unknown,
|
|
32
|
-
cb: (err: string | null, result: { value: number }) => void
|
|
33
|
-
) => void;
|
|
34
|
-
saveWorkout: (
|
|
35
|
-
opts: unknown,
|
|
36
|
-
cb: (err: string | null) => void
|
|
37
|
-
) => void;
|
|
38
|
-
Constants?: { Permissions?: Record<string, string> };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Android Health Connect (react-native-health-connect) ─────────────
|
|
42
|
-
interface HealthConnect {
|
|
43
|
-
initialize: () => Promise<boolean>;
|
|
44
|
-
requestPermission: (
|
|
45
|
-
permissions: { accessType: string; recordType: string }[]
|
|
46
|
-
) => Promise<{ accessType: string; recordType: string }[]>;
|
|
47
|
-
readRecords: (
|
|
48
|
-
recordType: string,
|
|
49
|
-
opts: { timeRangeFilter: { operator: string; startTime: string; endTime: string } }
|
|
50
|
-
) => Promise<{ records: { count: number }[] }>;
|
|
51
|
-
insertRecords: (records: unknown[]) => Promise<unknown>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let _appleHealth: AppleHealthKit | null | undefined = undefined;
|
|
55
|
-
let _healthConnect: HealthConnect | null | undefined = undefined;
|
|
56
|
-
|
|
57
|
-
function getAppleHealth(): AppleHealthKit | null {
|
|
58
|
-
if (Platform.OS !== 'ios') {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
if (_appleHealth !== undefined) {
|
|
62
|
-
return _appleHealth;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
-
const pkg = require('react-native-health');
|
|
67
|
-
_appleHealth = (pkg.default ?? pkg) as AppleHealthKit;
|
|
68
|
-
} catch {
|
|
69
|
-
_appleHealth = null;
|
|
70
|
-
}
|
|
71
|
-
return _appleHealth;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function getHealthConnect(): HealthConnect | null {
|
|
75
|
-
if (Platform.OS !== 'android') {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
if (_healthConnect !== undefined) {
|
|
79
|
-
return _healthConnect;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
83
|
-
_healthConnect = require('react-native-health-connect') as
|
|
84
|
-
HealthConnect;
|
|
85
|
-
} catch {
|
|
86
|
-
_healthConnect = null;
|
|
87
|
-
}
|
|
88
|
-
return _healthConnect;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const APPLE_PERMISSIONS = {
|
|
92
|
-
permissions: {
|
|
93
|
-
read: ['Steps'],
|
|
94
|
-
write: ['Workout']
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const ANDROID_PERMISSIONS = [
|
|
99
|
-
{ accessType: 'read', recordType: 'Steps' },
|
|
100
|
-
{ accessType: 'write', recordType: 'ExerciseSession' }
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Surface the platform's permission sheet. Returns true if the user
|
|
105
|
-
* granted enough permissions for our subsequent reads/writes to work.
|
|
106
|
-
*/
|
|
107
|
-
export async function requestHealthPermissions(): Promise<boolean> {
|
|
108
|
-
if (Platform.OS === 'ios') {
|
|
109
|
-
const ahk = getAppleHealth();
|
|
110
|
-
if (!ahk) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
return new Promise(resolve => {
|
|
114
|
-
ahk.initHealthKit(APPLE_PERMISSIONS, err => {
|
|
115
|
-
resolve(!err);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
if (Platform.OS === 'android') {
|
|
120
|
-
const hc = getHealthConnect();
|
|
121
|
-
if (!hc) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
const ready = await hc.initialize();
|
|
126
|
-
if (!ready) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
const granted = await hc.requestPermission(
|
|
130
|
-
ANDROID_PERMISSIONS
|
|
131
|
-
);
|
|
132
|
-
return granted.length === ANDROID_PERMISSIONS.length;
|
|
133
|
-
} catch {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export async function getStepsToday(): Promise<number> {
|
|
141
|
-
const start = new Date();
|
|
142
|
-
start.setHours(0, 0, 0, 0);
|
|
143
|
-
const end = new Date();
|
|
144
|
-
|
|
145
|
-
if (Platform.OS === 'ios') {
|
|
146
|
-
const ahk = getAppleHealth();
|
|
147
|
-
if (!ahk) {
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
|
-
return new Promise(resolve => {
|
|
151
|
-
ahk.getStepCount({ date: end.toISOString() }, (err, res) => {
|
|
152
|
-
if (err) {
|
|
153
|
-
resolve(0);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
resolve(res?.value ?? 0);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (Platform.OS === 'android') {
|
|
161
|
-
const hc = getHealthConnect();
|
|
162
|
-
if (!hc) {
|
|
163
|
-
return 0;
|
|
164
|
-
}
|
|
165
|
-
try {
|
|
166
|
-
const res = await hc.readRecords('Steps', {
|
|
167
|
-
timeRangeFilter: {
|
|
168
|
-
operator: 'between',
|
|
169
|
-
startTime: start.toISOString(),
|
|
170
|
-
endTime: end.toISOString()
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
return res.records.reduce((sum, r) => sum + (r.count ?? 0), 0);
|
|
174
|
-
} catch {
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface ExerciseSession {
|
|
182
|
-
type: string;
|
|
183
|
-
startedAt: Date;
|
|
184
|
-
durationMs: number;
|
|
185
|
-
energyBurned?: number;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Write a completed exercise session to the platform's health store.
|
|
190
|
-
* `type` is loosely free-form on iOS (Apple maps it to HKWorkoutType)
|
|
191
|
-
* and constrained on Android (must be a Health Connect ExerciseType).
|
|
192
|
-
* For barebones we leave the mapping to the caller — wrap this in a
|
|
193
|
-
* domain method like `recordYogaSession` if you only ever record one
|
|
194
|
-
* kind.
|
|
195
|
-
*/
|
|
196
|
-
export async function writeExerciseSession(
|
|
197
|
-
s: ExerciseSession
|
|
198
|
-
): Promise<boolean> {
|
|
199
|
-
const startISO = s.startedAt.toISOString();
|
|
200
|
-
const endISO = new Date(
|
|
201
|
-
s.startedAt.getTime() + s.durationMs
|
|
202
|
-
).toISOString();
|
|
203
|
-
|
|
204
|
-
if (Platform.OS === 'ios') {
|
|
205
|
-
const ahk = getAppleHealth();
|
|
206
|
-
if (!ahk) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
return new Promise(resolve => {
|
|
210
|
-
ahk.saveWorkout({
|
|
211
|
-
type: s.type,
|
|
212
|
-
startDate: startISO,
|
|
213
|
-
endDate: endISO,
|
|
214
|
-
energyBurned: s.energyBurned ?? 0,
|
|
215
|
-
energyBurnedUnit: 'kilocalorie'
|
|
216
|
-
}, err => resolve(!err));
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
if (Platform.OS === 'android') {
|
|
220
|
-
const hc = getHealthConnect();
|
|
221
|
-
if (!hc) {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
await hc.insertRecords([{
|
|
226
|
-
recordType: 'ExerciseSession',
|
|
227
|
-
exerciseType: s.type,
|
|
228
|
-
startTime: startISO,
|
|
229
|
-
endTime: endISO,
|
|
230
|
-
title: s.type
|
|
231
|
-
}]);
|
|
232
|
-
return true;
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return false;
|
|
238
|
-
}
|