Skip to content

Commit e3a7f22

Browse files
committed
refactor: implement renderPushPermissionPrompt and remove onBeforePushPermissionRequest
1 parent d9ac5d8 commit e3a7f22

File tree

6 files changed

+183
-144
lines changed

6 files changed

+183
-144
lines changed

README.md

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ Main provider component that enables sync functionality.
331331
| `syncMode` | `'polling' \| 'push'` | ❌ | Sync mode (default: `'polling'`) |
332332
| `adaptivePolling` | `AdaptivePollingConfig` | ❌ | Adaptive polling configuration (polling mode only) |
333333
| `notificationListening` | `'foreground' \| 'always'` | ❌ | When to listen for push notifications (push mode only) |
334-
| `onBeforePushPermissionRequest` | `() => Promise<boolean>` | ❌ | Custom UI before system permission prompt (push mode only) |
334+
| `renderPushPermissionPrompt` | `(props: { allow: () => void; deny: () => void }) => ReactNode` | ❌ | Render prop for permission prompt UI (push mode only) |
335335
| `onDatabaseReady` | `(db: DB) => Promise<void>` | ❌ | Callback after DB opens, before sync init (for migrations) |
336336
| `debug` | `boolean` | ❌ | Enable debug logging (default: `false`) |
337337
| `children` | `ReactNode` | ✅ | Child components |
@@ -439,54 +439,30 @@ Use `onDatabaseReady` to run migrations or other setup after the database opens
439439
>
440440
```
441441

442-
#### Custom Push Permission UI with `onBeforePushPermissionRequest`
442+
#### Custom Push Permission UI with `renderPushPermissionPrompt`
443443

444-
When using push mode, the system will prompt the user for notification permissions. Use `onBeforePushPermissionRequest` to show your own UI (e.g., an explanation modal) before the system prompt appears. Return `true` to proceed with the system prompt, or `false` to skip it.
444+
When using push mode, the system will prompt the user for notification permissions. Use `renderPushPermissionPrompt` to show your own UI before the system prompt appears. The render prop receives `allow` and `deny` callbacks — no Promise or ref boilerplate needed.
445445

446-
```typescript
447-
import { useState, useCallback, useRef } from 'react';
448-
import { Modal, View, Text, TouchableOpacity } from 'react-native';
449-
450-
export default function App() {
451-
const [showDialog, setShowDialog] = useState(false);
452-
const resolverRef = useRef<((value: boolean) => void) | null>(null);
453-
454-
const handleBeforePermission = useCallback(async () => {
455-
return new Promise<boolean>((resolve) => {
456-
resolverRef.current = resolve;
457-
setShowDialog(true);
458-
});
459-
}, []);
460-
461-
return (
462-
<>
463-
<Modal visible={showDialog} transparent>
464-
<View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
465-
<Text>Enable notifications for real-time sync?</Text>
466-
<TouchableOpacity onPress={() => {
467-
setShowDialog(false);
468-
resolverRef.current?.(true); // Proceed to system prompt
469-
}}>
470-
<Text>Enable</Text>
471-
</TouchableOpacity>
472-
<TouchableOpacity onPress={() => {
473-
setShowDialog(false);
474-
resolverRef.current?.(false); // Skip, fall back to polling
475-
}}>
476-
<Text>Not Now</Text>
477-
</TouchableOpacity>
478-
</View>
479-
</Modal>
480-
<SQLiteSyncProvider
481-
syncMode="push"
482-
onBeforePushPermissionRequest={handleBeforePermission}
483-
// ...other props
484-
>
485-
<YourApp />
486-
</SQLiteSyncProvider>
487-
</>
488-
);
489-
}
446+
```tsx
447+
<SQLiteSyncProvider
448+
syncMode="push"
449+
renderPushPermissionPrompt={({ allow, deny }) => (
450+
<Modal visible animationType="fade" transparent>
451+
<View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
452+
<Text>Enable notifications for real-time sync?</Text>
453+
<TouchableOpacity onPress={allow}>
454+
<Text>Enable</Text>
455+
</TouchableOpacity>
456+
<TouchableOpacity onPress={deny}>
457+
<Text>Not Now</Text>
458+
</TouchableOpacity>
459+
</View>
460+
</Modal>
461+
)}
462+
// ...other props
463+
>
464+
<YourApp />
465+
</SQLiteSyncProvider>
490466
```
491467
492468
#### `AdaptivePollingConfig`

examples/sync-demo-bare/src/App.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo } from 'react';
1+
import { useState, useMemo, useEffect } from 'react';
22
import {
33
Text,
44
View,
@@ -45,7 +45,27 @@ import {
4545
*/
4646
function TestApp() {
4747
const { writeDb, initError } = useSqliteDb();
48-
const { isSyncReady, isSyncing, lastSyncTime, syncError } = useSyncStatus();
48+
const { isSyncReady, isSyncing, lastSyncTime, syncError, currentSyncInterval } = useSyncStatus();
49+
const [nextSyncIn, setNextSyncIn] = useState<number | null>(null);
50+
51+
useEffect(() => {
52+
if (!lastSyncTime || !currentSyncInterval) {
53+
setNextSyncIn(null);
54+
return;
55+
}
56+
57+
const update = () => {
58+
const remaining = Math.max(
59+
0,
60+
Math.ceil((lastSyncTime + currentSyncInterval - Date.now()) / 1000)
61+
);
62+
setNextSyncIn(remaining);
63+
};
64+
65+
update();
66+
const id = setInterval(update, 1000);
67+
return () => clearInterval(id);
68+
}, [lastSyncTime, currentSyncInterval]);
4969
const [searchText, setSearchText] = useState('');
5070
const [text, setText] = useState('');
5171
const [rowNotification, setRowNotification] = useState<string | null>(null);
@@ -151,6 +171,11 @@ function TestApp() {
151171
Last sync: {new Date(lastSyncTime).toLocaleTimeString()}
152172
</Text>
153173
)}
174+
{nextSyncIn != null && (
175+
<Text style={styles.status}>
176+
Next sync in {nextSyncIn}s
177+
</Text>
178+
)}
154179
</View>
155180

156181
{/* SEARCH BAR */}

examples/sync-demo-expo/src/App.tsx

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
1+
import { useState, useMemo, useEffect } from 'react';
22
import {
33
Text,
44
View,
@@ -86,7 +86,27 @@ registerBackgroundSyncCallback(
8686
*/
8787
function TestApp({ deviceToken }: { deviceToken: string | null }) {
8888
const { writeDb, initError } = useSqliteDb();
89-
const { isSyncReady, isSyncing, lastSyncTime, syncError } = useSyncStatus();
89+
const { isSyncReady, isSyncing, lastSyncTime, syncError, currentSyncInterval } = useSyncStatus();
90+
const [nextSyncIn, setNextSyncIn] = useState<number | null>(null);
91+
92+
useEffect(() => {
93+
if (!lastSyncTime || !currentSyncInterval) {
94+
setNextSyncIn(null);
95+
return;
96+
}
97+
98+
const update = () => {
99+
const remaining = Math.max(
100+
0,
101+
Math.ceil((lastSyncTime + currentSyncInterval - Date.now()) / 1000)
102+
);
103+
setNextSyncIn(remaining);
104+
};
105+
106+
update();
107+
const id = setInterval(update, 1000);
108+
return () => clearInterval(id);
109+
}, [lastSyncTime, currentSyncInterval]);
90110
const [searchText, setSearchText] = useState('');
91111
const [text, setText] = useState('');
92112
const [rowNotification, setRowNotification] = useState<string | null>(null);
@@ -206,6 +226,11 @@ function TestApp({ deviceToken }: { deviceToken: string | null }) {
206226
Last sync: {new Date(lastSyncTime).toLocaleTimeString()}
207227
</Text>
208228
)}
229+
{nextSyncIn != null && (
230+
<Text style={styles.status}>
231+
Next sync in {nextSyncIn}s
232+
</Text>
233+
)}
209234
</View>
210235

211236
{/* SEARCH BAR */}
@@ -355,8 +380,6 @@ function PermissionDialog({
355380
}
356381

357382
export default function App() {
358-
const [showPermissionDialog, setShowPermissionDialog] = useState(false);
359-
const permissionResolverRef = useRef<((value: boolean) => void) | null>(null);
360383
const [deviceToken, setDeviceToken] = useState<string | null>(null);
361384

362385
useEffect(() => {
@@ -365,26 +388,6 @@ export default function App() {
365388
.catch(() => setDeviceToken('Failed to get token'));
366389
}, []);
367390

368-
// Callback to show custom UI before system permission request
369-
const handleBeforePushPermissionRequest = useCallback(async () => {
370-
return new Promise<boolean>((resolve) => {
371-
permissionResolverRef.current = resolve;
372-
setShowPermissionDialog(true);
373-
});
374-
}, []);
375-
376-
const handlePermissionAllow = useCallback(() => {
377-
setShowPermissionDialog(false);
378-
permissionResolverRef.current?.(true);
379-
permissionResolverRef.current = null;
380-
}, []);
381-
382-
const handlePermissionDeny = useCallback(() => {
383-
setShowPermissionDialog(false);
384-
permissionResolverRef.current?.(false);
385-
permissionResolverRef.current = null;
386-
}, []);
387-
388391
if (
389392
!SQLITE_CLOUD_CONNECTION_STRING ||
390393
!SQLITE_CLOUD_API_KEY ||
@@ -405,36 +408,31 @@ export default function App() {
405408
}
406409

407410
return (
408-
<>
409-
<PermissionDialog
410-
visible={showPermissionDialog}
411-
onAllow={handlePermissionAllow}
412-
onDeny={handlePermissionDeny}
413-
/>
414-
<SQLiteSyncProvider
415-
connectionString={SQLITE_CLOUD_CONNECTION_STRING}
416-
databaseName={DATABASE_NAME}
417-
tablesToBeSynced={[
418-
{
419-
name: TABLE_NAME,
420-
createTableSql: `
421-
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
422-
id TEXT PRIMARY KEY NOT NULL,
423-
value TEXT,
424-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
425-
);
426-
`,
427-
},
428-
]}
429-
syncMode="push"
430-
notificationListening="always"
431-
onBeforePushPermissionRequest={handleBeforePushPermissionRequest}
432-
apiKey={SQLITE_CLOUD_API_KEY}
433-
debug={true}
434-
>
435-
<TestApp deviceToken={deviceToken} />
436-
</SQLiteSyncProvider>
437-
</>
411+
<SQLiteSyncProvider
412+
connectionString={SQLITE_CLOUD_CONNECTION_STRING}
413+
databaseName={DATABASE_NAME}
414+
tablesToBeSynced={[
415+
{
416+
name: TABLE_NAME,
417+
createTableSql: `
418+
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
419+
id TEXT PRIMARY KEY NOT NULL,
420+
value TEXT,
421+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
422+
);
423+
`,
424+
},
425+
]}
426+
syncMode="push"
427+
notificationListening="always"
428+
renderPushPermissionPrompt={({ allow, deny }) => (
429+
<PermissionDialog visible onAllow={allow} onDeny={deny} />
430+
)}
431+
apiKey={SQLITE_CLOUD_API_KEY}
432+
debug={true}
433+
>
434+
<TestApp deviceToken={deviceToken} />
435+
</SQLiteSyncProvider>
438436
);
439437
}
440438

src/core/SQLiteSyncProvider.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function SQLiteSyncProvider({
6363
adaptivePolling,
6464
syncMode = 'polling',
6565
notificationListening = 'foreground',
66-
onBeforePushPermissionRequest,
66+
renderPushPermissionPrompt,
6767
onDatabaseReady,
6868
debug = false,
6969
children,
@@ -147,12 +147,13 @@ export function SQLiteSyncProvider({
147147
});
148148

149149
/** RESET INTERVAL ON CONFIG CHANGE */
150-
const prevSyncModeRef = useRef(syncMode);
150+
const prevEffectiveSyncModeRef = useRef(effectiveSyncMode);
151151
const prevIdleMultiplierRef = useRef(adaptiveConfig.idleBackoffMultiplier);
152152
const prevErrorMultiplierRef = useRef(adaptiveConfig.errorBackoffMultiplier);
153153

154154
useEffect(() => {
155-
const syncModeChanged = prevSyncModeRef.current !== syncMode;
155+
const syncModeChanged =
156+
prevEffectiveSyncModeRef.current !== effectiveSyncMode;
156157
const idleChanged =
157158
prevIdleMultiplierRef.current !== adaptiveConfig.idleBackoffMultiplier;
158159
const errorChanged =
@@ -161,7 +162,7 @@ export function SQLiteSyncProvider({
161162
if (syncModeChanged || idleChanged || errorChanged) {
162163
if (syncModeChanged) {
163164
logger.info(
164-
`🔄 Sync mode changed to '${syncMode}' - resetting interval state`
165+
`🔄 Sync mode changed to '${effectiveSyncMode}' - resetting interval state`
165166
);
166167
} else {
167168
logger.info(
@@ -171,20 +172,20 @@ export function SQLiteSyncProvider({
171172

172173
setConsecutiveEmptySyncs(0);
173174

174-
if (syncMode === 'polling') {
175+
if (effectiveSyncMode === 'polling') {
175176
setCurrentInterval(adaptiveConfig.baseInterval);
176177
currentIntervalRef.current = adaptiveConfig.baseInterval;
177178
} else {
178179
setCurrentInterval(null);
179180
currentIntervalRef.current = null;
180181
}
181182

182-
prevSyncModeRef.current = syncMode;
183+
prevEffectiveSyncModeRef.current = effectiveSyncMode;
183184
prevIdleMultiplierRef.current = adaptiveConfig.idleBackoffMultiplier;
184185
prevErrorMultiplierRef.current = adaptiveConfig.errorBackoffMultiplier;
185186
}
186187
}, [
187-
syncMode,
188+
effectiveSyncMode,
188189
adaptiveConfig.idleBackoffMultiplier,
189190
adaptiveConfig.errorBackoffMultiplier,
190191
adaptiveConfig.baseInterval,
@@ -237,15 +238,15 @@ export function SQLiteSyncProvider({
237238
}, [logger]);
238239

239240
/** PUSH NOTIFICATIONS - Only active when syncMode is 'push' */
240-
usePushNotificationSync({
241+
const { permissionPromptNode } = usePushNotificationSync({
241242
isSyncReady,
242243
performSyncRef,
243244
writeDbRef,
244245
syncMode: effectiveSyncMode,
245246
notificationListening,
246247
logger,
247248
onPermissionsDenied: handlePermissionsDenied,
248-
onBeforePushPermissionRequest,
249+
renderPushPermissionPrompt,
249250
connectionString,
250251
databaseName,
251252
tablesToBeSynced,
@@ -314,6 +315,7 @@ export function SQLiteSyncProvider({
314315
<SQLiteSyncStatusContext.Provider value={syncStatusContextValue}>
315316
<SQLiteSyncActionsContext.Provider value={syncActionsContextValue}>
316317
{children}
318+
{permissionPromptNode}
317319
</SQLiteSyncActionsContext.Provider>
318320
</SQLiteSyncStatusContext.Provider>
319321
</SQLiteDbContext.Provider>

0 commit comments

Comments
 (0)