Skip to content

Commit 6e39f6c

Browse files
committed
feat: add sneekpeak functionality
1 parent 1820f2c commit 6e39f6c

3 files changed

Lines changed: 225 additions & 0 deletions

File tree

apps/example/screens/AllTheThings.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,83 @@ export function AllTheThings() {
285285
}}
286286
/>
287287

288+
<Button
289+
title="Test sneakpeak"
290+
onPress={() => {
291+
/*ReactNativeDeviceActivity.configureActions({
292+
activityName: "sneek-peak",
293+
callbackName: "intervalDidEnd",
294+
actions: [
295+
{
296+
type: "sendNotification",
297+
payload: {
298+
title: "sneek-peak ended!!!",
299+
body: "You have reached the end of the sneek-peak!",
300+
},
301+
},
302+
{
303+
type: "enableBlockAllMode",
304+
},
305+
{
306+
type: "clearWhitelistAndUpdateBlock",
307+
},
308+
],
309+
});*/
310+
311+
ReactNativeDeviceActivity.configureActions({
312+
activityName: "sneekpeak",
313+
callbackName: "intervalDidStart",
314+
actions: [
315+
{
316+
type: "sendNotification",
317+
payload: {
318+
title: "sneek-peak ended!!!",
319+
body: "You have reached the end of the sneek-peak!",
320+
},
321+
},
322+
{
323+
type: "enableBlockAllMode",
324+
},
325+
{
326+
type: "clearWhitelistAndUpdateBlock",
327+
},
328+
{
329+
type: "stopMonitoring",
330+
activityNames: ["sneekpeak"],
331+
}
332+
],
333+
});
334+
ReactNativeDeviceActivity.updateShield(
335+
{
336+
title: "take a sneekpeak!",
337+
primaryButtonLabel: "do it now!",
338+
},
339+
{
340+
primary: {
341+
behavior: "defer",
342+
actions: [
343+
{
344+
type: "disableBlockAllMode",
345+
},
346+
{
347+
type: "clearWhitelistAndUpdateBlock",
348+
},
349+
{
350+
type: "startMonitoring",
351+
activityName: "sneekpeak",
352+
intervalStartDelayMs: 10000,
353+
intervalEndDelayMs: 60 * 15 * 1000 + 10000,
354+
deviceActivityEvents: [],
355+
},
356+
],
357+
},
358+
},
359+
);
360+
console.log("done!");
361+
console.log(ReactNativeDeviceActivity.getEvents())
362+
}}
363+
/>
364+
288365
<TextInput
289366
placeholder="Enter shield title"
290367
onChangeText={(text) => setShieldTitle(text)}

packages/react-native-device-activity/ios/Shared.swift

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,25 @@ func executeGenericAction(
238238
// required for it to have time to trigger before process/callback ends
239239
sleep(ms: 1000)
240240
}
241+
} else if type == "startMonitoring" {
242+
if let activityName = action["activityName"] as? String,
243+
let deviceActivityEvents = action["deviceActivityEvents"] as? [[String: Any]] {
244+
245+
startMonitoringAction(
246+
activityName: activityName,
247+
deviceActivityEvents: deviceActivityEvents,
248+
intervalStartDelayMs: action["intervalStartDelayMs"] as? Int,
249+
intervalEndDelayMs: action["intervalEndDelayMs"] as? Int,
250+
triggeredBy: triggeredBy
251+
)
252+
}
253+
} else if type == "stopMonitoring" {
254+
let activityNames = action["activityNames"] as? [String]
255+
256+
stopMonitoringAction(
257+
activityNames: activityNames,
258+
triggeredBy: triggeredBy
259+
)
241260
}
242261

243262
if let sleepAfter = action["sleepAfter"] as? Int {
@@ -1423,3 +1442,114 @@ func getAppGroupDirectory() -> URL? {
14231442
}
14241443
return nil
14251444
}
1445+
1446+
@available(iOS 15.0, *)
1447+
func startMonitoringAction(
1448+
activityName: String,
1449+
deviceActivityEvents: [[String: Any]],
1450+
intervalStartDelayMs: Int?,
1451+
intervalEndDelayMs: Int?,
1452+
triggeredBy: String
1453+
) {
1454+
// Create date components for schedule
1455+
let now = Date()
1456+
let calendar = Calendar.current
1457+
1458+
var intervalStart = DateComponents()
1459+
var intervalEnd = DateComponents()
1460+
1461+
if let startDelayMs = intervalStartDelayMs {
1462+
let startDate = now.addingTimeInterval(TimeInterval(startDelayMs) / 1000.0)
1463+
intervalStart = calendar.dateComponents([.hour, .minute, .second], from: startDate)
1464+
}
1465+
1466+
if let endDelayMs = intervalEndDelayMs {
1467+
let endDate = now.addingTimeInterval(TimeInterval(endDelayMs) / 1000.0)
1468+
intervalEnd = calendar.dateComponents([.hour, .minute, .second], from: endDate)
1469+
} else {
1470+
// Default to 24 hours from start if not specified
1471+
let defaultEndMs = (intervalStartDelayMs ?? 0) + (24 * 60 * 60 * 1000)
1472+
let endDate = now.addingTimeInterval(TimeInterval(defaultEndMs) / 1000.0)
1473+
intervalEnd = calendar.dateComponents([.hour, .minute, .second], from: endDate)
1474+
}
1475+
1476+
let schedule = DeviceActivitySchedule(
1477+
intervalStart: intervalStart,
1478+
intervalEnd: intervalEnd,
1479+
repeats: false
1480+
)
1481+
1482+
// Create DeviceActivityEvent dictionary
1483+
var eventDict: [DeviceActivityEvent.Name: DeviceActivityEvent] = [:]
1484+
1485+
for eventData in deviceActivityEvents {
1486+
guard let eventName = eventData["eventName"] as? String,
1487+
let threshold = eventData["threshold"] as? [String: Any],
1488+
let familyActivitySelection = eventData["familyActivitySelection"] as? String
1489+
else {
1490+
continue
1491+
}
1492+
1493+
let selection = deserializeFamilyActivitySelection(
1494+
familyActivitySelectionStr: familyActivitySelection)
1495+
1496+
// Convert threshold to DateComponents
1497+
var thresholdComponents = DateComponents()
1498+
if let hour = threshold["hour"] as? Int { thresholdComponents.hour = hour }
1499+
if let minute = threshold["minute"] as? Int { thresholdComponents.minute = minute }
1500+
if let second = threshold["second"] as? Int { thresholdComponents.second = second }
1501+
1502+
let includesPastActivity = eventData["includesPastActivity"] as? Bool ?? false
1503+
1504+
var event: DeviceActivityEvent
1505+
if #available(iOS 17.4, *) {
1506+
event = DeviceActivityEvent(
1507+
applications: selection.applicationTokens,
1508+
categories: selection.categoryTokens,
1509+
webDomains: selection.webDomainTokens,
1510+
threshold: thresholdComponents,
1511+
includesPastActivity: includesPastActivity
1512+
)
1513+
} else {
1514+
event = DeviceActivityEvent(
1515+
applications: selection.applicationTokens,
1516+
categories: selection.categoryTokens,
1517+
webDomains: selection.webDomainTokens,
1518+
threshold: thresholdComponents
1519+
)
1520+
}
1521+
1522+
eventDict[DeviceActivityEvent.Name(eventName)] = event
1523+
}
1524+
1525+
let activityName = DeviceActivityName(activityName)
1526+
1527+
do {
1528+
try center.startMonitoring(activityName, during: schedule, events: eventDict)
1529+
logger.log(
1530+
"✅ Successfully started monitoring activity: \(activityName.rawValue) from \(triggeredBy)")
1531+
} catch {
1532+
logger.log(
1533+
"❌ Failed to start monitoring activity: \(activityName.rawValue, privacy: .public) - \(error.localizedDescription, privacy: .public)"
1534+
)
1535+
}
1536+
}
1537+
1538+
@available(iOS 15.0, *)
1539+
func stopMonitoringAction(
1540+
activityNames: [String]?,
1541+
triggeredBy: String
1542+
) {
1543+
if let activityNames = activityNames {
1544+
// Stop specific activities
1545+
let deviceActivityNames = activityNames.map { DeviceActivityName($0) }
1546+
center.stopMonitoring(deviceActivityNames)
1547+
logger.log(
1548+
"✅ Successfully stopped monitoring activities: \(activityNames.joined(separator: ", ")) from \(triggeredBy)"
1549+
)
1550+
} else {
1551+
// Stop all monitoring
1552+
center.stopMonitoring()
1553+
logger.log("✅ Successfully stopped all monitoring from \(triggeredBy)")
1554+
}
1555+
}

packages/react-native-device-activity/src/ReactNativeDeviceActivity.types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,24 @@ export type Action =
287287
} & CommonTypeParams)
288288
| ({
289289
type: "removeAllDeliveredNotifications";
290+
} & CommonTypeParams)
291+
| ({
292+
type: "startMonitoring";
293+
activityName: string;
294+
deviceActivityEvents: DeviceActivityEvent[];
295+
/**
296+
* Optional delay in milliseconds from now for intervalStart.
297+
* If provided, will override deviceActivitySchedule.intervalStart.
298+
*/
299+
intervalStartDelayMs?: number;
300+
intervalEndDelayMs?: number;
301+
} & CommonTypeParams)
302+
| ({
303+
type: "stopMonitoring";
304+
/**
305+
* Optional array of activity names to stop. If not provided, stops all monitoring.
306+
*/
307+
activityNames?: string[];
290308
} & CommonTypeParams);
291309

292310
export type DeviceActivityEventRaw = Omit<

0 commit comments

Comments
 (0)