diff --git a/example/App.tsx b/example/App.tsx
index 1adce40a..27157073 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -216,9 +216,8 @@ export default function App() {
Select apps
+
{JSON.stringify(events, null, 2)}
{JSON.stringify(activities, null, 2)}
diff --git a/example/ios/DeviceActivityReport/DeviceActivityReport.entitlements b/example/ios/DeviceActivityReport/DeviceActivityReport.entitlements
new file mode 100644
index 00000000..c7caaff4
--- /dev/null
+++ b/example/ios/DeviceActivityReport/DeviceActivityReport.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.developer.family-controls
+
+
+
diff --git a/example/ios/DeviceActivityReport/DeviceActivityReport.swift b/example/ios/DeviceActivityReport/DeviceActivityReport.swift
new file mode 100644
index 00000000..7d534d1f
--- /dev/null
+++ b/example/ios/DeviceActivityReport/DeviceActivityReport.swift
@@ -0,0 +1,20 @@
+//
+// DeviceActivityReport.swift
+// DeviceActivityReport
+//
+// Created by Robert Herber on 2024-11-10.
+//
+
+import DeviceActivity
+import SwiftUI
+
+@main
+struct DeviceActivityReportUI: DeviceActivityReportExtension {
+ var body: some DeviceActivityReportScene {
+ // Create a report for each DeviceActivityReport.Context that your app supports.
+ TotalActivityReport { totalActivity in
+ TotalActivityView(totalActivity: totalActivity)
+ }
+ // Add more reports here...
+ }
+}
diff --git a/example/ios/DeviceActivityReport/Info.plist b/example/ios/DeviceActivityReport/Info.plist
new file mode 100644
index 00000000..5c599a43
--- /dev/null
+++ b/example/ios/DeviceActivityReport/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ EXAppExtensionAttributes
+
+ EXExtensionPointIdentifier
+ com.apple.deviceactivityui.report-extension
+
+
+
diff --git a/example/ios/DeviceActivityReport/TotalActivityReport.swift b/example/ios/DeviceActivityReport/TotalActivityReport.swift
new file mode 100644
index 00000000..a8faa68f
--- /dev/null
+++ b/example/ios/DeviceActivityReport/TotalActivityReport.swift
@@ -0,0 +1,46 @@
+//
+// TotalActivityReport.swift
+// DeviceActivityReport
+//
+// Created by Robert Herber on 2024-11-10.
+//
+
+import DeviceActivity
+import SwiftUI
+
+extension DeviceActivityReport.Context {
+ static let totalActivity = DeviceActivityReport.Context("Total Activity")
+}
+
+struct TotalActivityReport: DeviceActivityReportScene {
+ // Define which context your scene will represent.
+ let context: DeviceActivityReport.Context = .totalActivity
+
+ // Define the custom configuration and the resulting view for this report.
+ let content: (String) -> TotalActivityView
+
+ func makeConfiguration(representing data: DeviceActivityResults) async -> String {
+ // Reformat the data into a configuration that can be used to create
+ // the report's view.
+ let formatter = DateComponentsFormatter()
+ formatter.allowedUnits = [.day, .hour, .minute, .second]
+ formatter.unitsStyle = .abbreviated
+ formatter.zeroFormattingBehavior = .dropAll
+
+ let totalActivityDuration = await data.flatMap { $0.activitySegments }.reduce(0, {
+ $0 + $1.totalActivityDuration
+ })
+
+ /* let names = data.flatMap { point in
+ point.activitySegments.flatMap { segment in
+ segment.categories.flatMap { category in
+ category.applications.map { application in
+ application.application.localizedDisplayName
+ }
+ }
+ }
+ } */
+
+ return "\(String(describing: formatter.string(from: totalActivityDuration)))"
+ }
+}
diff --git a/example/ios/DeviceActivityReport/TotalActivityView.swift b/example/ios/DeviceActivityReport/TotalActivityView.swift
new file mode 100644
index 00000000..84ac7964
--- /dev/null
+++ b/example/ios/DeviceActivityReport/TotalActivityView.swift
@@ -0,0 +1,23 @@
+//
+// TotalActivityView.swift
+// DeviceActivityReport
+//
+// Created by Robert Herber on 2024-11-10.
+//
+
+import SwiftUI
+
+struct TotalActivityView: View {
+ let totalActivity: String
+
+ var body: some View {
+ Text(totalActivity)
+ }
+}
+
+// In order to support previews for your extension's custom views, make sure its source files are
+// members of your app's Xcode target as well as members of your extension's target. You can use
+// Xcode's File Inspector to modify a file's Target Membership.
+#Preview {
+ TotalActivityView(totalActivity: "1h 23m")
+}
diff --git a/example/ios/reactnativedeviceactivityexample.xcodeproj/project.pbxproj b/example/ios/reactnativedeviceactivityexample.xcodeproj/project.pbxproj
index 1db4e5df..3cfee3b2 100644
--- a/example/ios/reactnativedeviceactivityexample.xcodeproj/project.pbxproj
+++ b/example/ios/reactnativedeviceactivityexample.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -20,6 +20,8 @@
7196272736B8369F7352BB3C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 86C617632690A9F3C5D768FE /* PrivacyInfo.xcprivacy */; };
96905EF65AED1B983A6B3ABC /* libPods-reactnativedeviceactivityexample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-reactnativedeviceactivityexample.a */; };
984C7C2718574054BF6481E3 /* ShieldConfiguration.entitlements in Sources */ = {isa = PBXBuildFile; fileRef = 630F9328E9134A4D9F0874A2 /* ShieldConfiguration.entitlements */; };
+ A965D3EB2CE02FE400BC6C7C /* DeviceActivityReport.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = A965D3DF2CE02FE400BC6C7C /* DeviceActivityReport.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ A965D3F12CE0321000BC6C7C /* DeviceActivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E01EDC40AAA54847A43C3F99 /* DeviceActivity.framework */; };
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
B4F86CBB9D3C43A59EBE2323 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C383BD5E2049BBBED22172 /* Utils.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
@@ -40,6 +42,13 @@
remoteGlobalIDString = 1B33B275DB45484EAECB1B1B;
remoteInfo = ShieldConfiguration;
};
+ A965D3E92CE02FE400BC6C7C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = A965D3DE2CE02FE400BC6C7C;
+ remoteInfo = DeviceActivityReport;
+ };
B8AF53079E4145D686123C11 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
@@ -70,6 +79,17 @@
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
+ A965D3F02CE02FE400BC6C7C /* Embed ExtensionKit Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "$(EXTENSIONS_FOLDER_PATH)";
+ dstSubfolderSpec = 16;
+ files = (
+ A965D3EB2CE02FE400BC6C7C /* DeviceActivityReport.appex in Embed ExtensionKit Extensions */,
+ );
+ name = "Embed ExtensionKit Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -96,6 +116,7 @@
84457EAD9B9F41CDA5B58B4E /* ActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; fileEncoding = 4; includeInIndex = 0; path = ActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
86C617632690A9F3C5D768FE /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../targets/ShieldConfiguration/PrivacyInfo.xcprivacy; sourceTree = ""; };
98E197012252434F88CCEEB4 /* ActivityMonitorExtension.entitlements */ = {isa = PBXFileReference; explicitFileType = text.plist.entitlements; fileEncoding = 4; includeInIndex = 0; path = ActivityMonitorExtension.entitlements; sourceTree = ""; };
+ A965D3DF2CE02FE400BC6C7C /* DeviceActivityReport.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = DeviceActivityReport.appex; sourceTree = BUILT_PRODUCTS_DIR; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = reactnativedeviceactivityexample/SplashScreen.storyboard; sourceTree = ""; };
AEB321985C804DE4AB33EC69 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
BA3FB2A82413462F881AEB5A /* reactnativedeviceactivityexample-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "reactnativedeviceactivityexample-Bridging-Header.h"; path = "reactnativedeviceactivityexample/reactnativedeviceactivityexample-Bridging-Header.h"; sourceTree = ""; };
@@ -108,6 +129,20 @@
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-reactnativedeviceactivityexample/ExpoModulesProvider.swift"; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ A965D3EC2CE02FE400BC6C7C /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = A965D3DE2CE02FE400BC6C7C /* DeviceActivityReport */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ A965D3E02CE02FE400BC6C7C /* DeviceActivityReport */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (A965D3EC2CE02FE400BC6C7C /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = DeviceActivityReport; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -125,6 +160,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ A965D3DC2CE02FE400BC6C7C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A965D3F12CE0321000BC6C7C /* DeviceActivity.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
C16C057284E44BB4A80B1F4A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -196,6 +239,7 @@
C6BB3FB112304AFCB4071189 /* expo:targets */,
13B07FAE1A68108700A75B9A /* reactnativedeviceactivityexample */,
832341AE1AAA6A7D00B99B32 /* Libraries */,
+ A965D3E02CE02FE400BC6C7C /* DeviceActivityReport */,
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
D65327D7A22EEC0BE12398D9 /* Pods */,
@@ -213,6 +257,7 @@
EEEB0A38EA724033BF060181 /* ShieldConfiguration.appex */,
55BEF032CC20441DA94ED74D /* ShieldAction.appex */,
84457EAD9B9F41CDA5B58B4E /* ActivityMonitorExtension.appex */,
+ A965D3DF2CE02FE400BC6C7C /* DeviceActivityReport.appex */,
);
name = Products;
sourceTree = "";
@@ -318,6 +363,7 @@
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
3DB52EB52D2348B6B33DCF43 /* Embed Foundation Extensions */,
DC6C57584D7EC65201A20BD6 /* [CP] Embed Pods Frameworks */,
+ A965D3F02CE02FE400BC6C7C /* Embed ExtensionKit Extensions */,
);
buildRules = (
);
@@ -325,6 +371,7 @@
09A35F3A3CA14FF7910A7D62 /* PBXTargetDependency */,
E79F7418C95C4FDF86F2AC3A /* PBXTargetDependency */,
8D395BFCE4984204A074EC2E /* PBXTargetDependency */,
+ A965D3EA2CE02FE400BC6C7C /* PBXTargetDependency */,
);
name = reactnativedeviceactivityexample;
productName = reactnativedeviceactivityexample;
@@ -348,6 +395,28 @@
productReference = EEEB0A38EA724033BF060181 /* ShieldConfiguration.appex */;
productType = "com.apple.product-type.app-extension";
};
+ A965D3DE2CE02FE400BC6C7C /* DeviceActivityReport */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = A965D3ED2CE02FE400BC6C7C /* Build configuration list for PBXNativeTarget "DeviceActivityReport" */;
+ buildPhases = (
+ A965D3DB2CE02FE400BC6C7C /* Sources */,
+ A965D3DC2CE02FE400BC6C7C /* Frameworks */,
+ A965D3DD2CE02FE400BC6C7C /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ A965D3E02CE02FE400BC6C7C /* DeviceActivityReport */,
+ );
+ name = DeviceActivityReport;
+ packageProductDependencies = (
+ );
+ productName = DeviceActivityReport;
+ productReference = A965D3DF2CE02FE400BC6C7C /* DeviceActivityReport.appex */;
+ productType = "com.apple.product-type.extensionkit-extension";
+ };
B020639A1E02498DA97B7121 /* ActivityMonitorExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 70B6388B62644A0AB504262B /* Build configuration list for PBXNativeTarget "ActivityMonitorExtension" */;
@@ -371,6 +440,7 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
+ LastSwiftUpdateCheck = 1610;
LastUpgradeCheck = 1130;
TargetAttributes = {
09AF28B634D54463AF9E2548 = {
@@ -386,6 +456,9 @@
DevelopmentTeam = 34SE8X7Q58;
ProvisioningStyle = Automatic;
};
+ A965D3DE2CE02FE400BC6C7C = {
+ CreatedOnToolsVersion = 16.1;
+ };
B020639A1E02498DA97B7121 = {
CreatedOnToolsVersion = 14.3;
DevelopmentTeam = 34SE8X7Q58;
@@ -410,6 +483,7 @@
1B33B275DB45484EAECB1B1B /* ShieldConfiguration */,
09AF28B634D54463AF9E2548 /* ShieldAction */,
B020639A1E02498DA97B7121 /* ActivityMonitorExtension */,
+ A965D3DE2CE02FE400BC6C7C /* DeviceActivityReport */,
);
};
/* End PBXProject section */
@@ -433,6 +507,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ A965D3DD2CE02FE400BC6C7C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
B7A47D5A75F34E79911489AE /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -576,6 +657,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ A965D3DB2CE02FE400BC6C7C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
C5969BCCB0EE45DCB9BBF01A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -609,6 +697,11 @@
target = B020639A1E02498DA97B7121 /* ActivityMonitorExtension */;
targetProxy = C6D619B1FE3F43EEBE3A40C2 /* PBXContainerItemProxy */;
};
+ A965D3EA2CE02FE400BC6C7C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = A965D3DE2CE02FE400BC6C7C /* DeviceActivityReport */;
+ targetProxy = A965D3E92CE02FE400BC6C7C /* PBXContainerItemProxy */;
+ };
E79F7418C95C4FDF86F2AC3A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 09AF28B634D54463AF9E2548 /* ShieldAction */;
@@ -632,7 +725,7 @@
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = reactnativedeviceactivityexample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -664,7 +757,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 34SE8X7Q58;
INFOPLIST_FILE = reactnativedeviceactivityexample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 16;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -1020,6 +1113,91 @@
};
name = Debug;
};
+ A965D3EE2CE02FE400BC6C7C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = DeviceActivityReport/DeviceActivityReport.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = 34SE8X7Q58;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DeviceActivityReport/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = DeviceActivityReportUI;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 18.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = expo.modules.deviceactivity.example.DeviceActivityReportUI;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ A965D3EF2CE02FE400BC6C7C /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_ENTITLEMENTS = DeviceActivityReport/DeviceActivityReport.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = 34SE8X7Q58;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DeviceActivityReport/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = DeviceActivityReportUI;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 18.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = expo.modules.deviceactivity.example.DeviceActivityReportUI;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
AE37E16FFF5F42278976AF2A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -1109,6 +1287,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ A965D3ED2CE02FE400BC6C7C /* Build configuration list for PBXNativeTarget "DeviceActivityReport" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A965D3EE2CE02FE400BC6C7C /* Debug */,
+ A965D3EF2CE02FE400BC6C7C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
diff --git a/ios/DeviceActivityReportView.swift b/ios/DeviceActivityReportView.swift
new file mode 100644
index 00000000..d042aadd
--- /dev/null
+++ b/ios/DeviceActivityReportView.swift
@@ -0,0 +1,106 @@
+//
+// DeviceActivityReport.swift
+// Pods
+//
+// Created by Robert Herber on 2024-11-12.
+//
+
+import Foundation
+import FamilyControls
+import DeviceActivity
+import SwiftUI
+import Combine
+import ExpoModulesCore
+
+
+@available(iOS 16.0, *)
+class DeviceActivityReportViewModel: ObservableObject {
+ @Published var familyActivitySelection = FamilyActivitySelection()
+
+ @Published var devices = DeviceActivityFilter.Devices(Set())
+
+ @Published var users: DeviceActivityFilter.Users? = .all
+
+ @Published var context = "Total Activity"
+
+ @Published var from = Date.distantPast
+
+ @Published var to = Date.distantPast
+
+ // Public property for setting the string value
+ @Published var segmentation: String = "daily"
+
+ // Computed property that converts to SegmentInterval
+ var segment: DeviceActivityFilter.SegmentInterval {
+ let interval = DateInterval(start: from, end: to)
+
+ if(self.segmentation == "hourly"){
+ return .hourly(during: interval)
+ } else if (self.segmentation == "weekly"){
+ return .weekly(during: interval)
+ } else {
+ return .daily(during: interval)
+ }
+ }
+
+ init() { }
+}
+
+
+@available(iOS 16.0, *)
+struct DeviceActivityReportUI: View {
+ @ObservedObject var model: DeviceActivityReportViewModel
+
+ var body: some View {
+ DeviceActivityReport(
+ DeviceActivityReport.Context(rawValue: model.context), // the context of your extension
+ filter: model.users != nil ? DeviceActivityFilter(
+ segment: model.segment,
+ users: model.users!, // or .children
+ devices: model.devices,
+ applications: model.familyActivitySelection.applicationTokens,
+ categories: model.familyActivitySelection.categoryTokens,
+ webDomains: model.familyActivitySelection.webDomainTokens
+ // you can decide which kind of data to show - apps, categories, and/or web domains
+ ) : DeviceActivityFilter(
+ segment: model.segment,
+ devices: model.devices,
+ applications: model.familyActivitySelection.applicationTokens,
+ categories: model.familyActivitySelection.categoryTokens,
+ webDomains: model.familyActivitySelection.webDomainTokens
+ // you can decide which kind of data to show - apps, categories, and/or web domains
+ )
+ )
+ }
+}
+
+
+
+// This view will be used as a native component. Make sure to inherit from `ExpoView`
+// to apply the proper styling (e.g. border radius and shadows).
+@available(iOS 16.0, *)
+class DeviceActivityReportView: ExpoView {
+
+ public let model = DeviceActivityReportViewModel()
+
+ let contentView: UIHostingController
+
+ required init(appContext: AppContext? = nil) {
+ contentView = UIHostingController(
+ rootView: DeviceActivityReportUI(
+ model: model
+ )
+ )
+
+ super.init(appContext: appContext)
+
+ clipsToBounds = true
+
+ self.addSubview(contentView.view)
+ }
+
+ override func layoutSubviews() {
+ contentView.view.frame = bounds
+ }
+
+}
diff --git a/ios/ReactNativeDeviceActivityModule.swift b/ios/ReactNativeDeviceActivityModule.swift
index cfb24a60..a1b39550 100644
--- a/ios/ReactNativeDeviceActivityModule.swift
+++ b/ios/ReactNativeDeviceActivityModule.swift
@@ -166,270 +166,312 @@ class NativeEventObserver {
}
}
-@available(iOS 15.0, *)
+@available(iOS 16.0, *)
public class ReactNativeDeviceActivityModule: Module {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
- public func definition() -> ModuleDefinition {
- // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
- // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
- // The module will be accessible from `requireNativeModule('ReactNativeDeviceActivity')` in JavaScript.
- Name("ReactNativeDeviceActivity")
-
- let center = DeviceActivityCenter()
-
-
- // Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
- //Constants([
- // "PI": Double.pi
- //])
-
- let observer = NativeEventObserver(module: self)
-
- var userDefaults = UserDefaults(suiteName: "group.ActivityMonitor")
-
- Function("setAppGroup") { (appGroup: String) in
- userDefaults = UserDefaults(suiteName: appGroup)
- }
-
- Function("getEvents") { (activityName: String?) -> [AnyHashable: Any] in
-
- let dict = userDefaults?.dictionaryRepresentation()
-
- guard let actualDict = dict else {
- return [:] // Return an empty dictionary instead of an empty array
- }
-
- let filteredDict = actualDict.filter({ (key: String, value: Any) in
- return key.starts(with: activityName == nil ? "DeviceActivityMonitorExtension#" : "DeviceActivityMonitorExtension#\(activityName!)#")
- }).reduce(into: [:]) { (result, element) in
- let (key, value) = element
- result[key] = value as? NSNumber // Add key-value pair to the result dictionary
- }
-
- return filteredDict
- }
-
- Function("doesSelectionHaveOverlap") { (familyActivitySelections: [String]) in
- let decodedFamilyActivitySelections: [FamilyActivitySelection] = familyActivitySelections.map { familyActivitySelection in
- let decoder = JSONDecoder()
- let data = Data(base64Encoded: familyActivitySelection)
- do {
- let activitySelection = try decoder.decode(FamilyActivitySelection.self, from: data!)
- return activitySelection
+ public func definition() -> ModuleDefinition {
+ // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
+ // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
+ // The module will be accessible from `requireNativeModule('ReactNativeDeviceActivity')` in JavaScript.
+ Name("ReactNativeDeviceActivity")
+
+ let center = DeviceActivityCenter()
+
+
+ // Sets constant properties on the module. Can take a dictionary or a closure that returns a dictionary.
+ //Constants([
+ // "PI": Double.pi
+ //])
+
+ let observer = NativeEventObserver(module: self)
+
+ var userDefaults = UserDefaults(suiteName: "group.ActivityMonitor")
+
+ Function("setAppGroup") { (appGroup: String) in
+ userDefaults = UserDefaults(suiteName: appGroup)
+ }
+
+ Function("getEvents") { (activityName: String?) -> [AnyHashable: Any] in
+
+ let dict = userDefaults?.dictionaryRepresentation()
+
+ guard let actualDict = dict else {
+ return [:] // Return an empty dictionary instead of an empty array
}
- catch {
- return FamilyActivitySelection()
+
+ let filteredDict = actualDict.filter({ (key: String, value: Any) in
+ return key.starts(with: activityName == nil ? "DeviceActivityMonitorExtension#" : "DeviceActivityMonitorExtension#\(activityName!)#")
+ }).reduce(into: [:]) { (result, element) in
+ let (key, value) = element
+ result[key] = value as? NSNumber // Add key-value pair to the result dictionary
}
- }
-
- let hasOverlap = decodedFamilyActivitySelections.contains { selection in
- return decodedFamilyActivitySelections.contains { compareWith in
- // if it's the same instance - skip comparison
- if(compareWith == selection){
- return false
- }
-
- if(compareWith.applicationTokens.contains(where: { token in
- return selection.applicationTokens.contains(token)
- } )){
- return true
- }
-
- if(compareWith.categoryTokens.contains(where: { token in
- return selection.categoryTokens.contains(token)
- } )){
- return true
- }
-
- if(compareWith.webDomainTokens.contains(where: { token in
- return selection.webDomainTokens.contains(token)
- } )){
- return true
- }
-
- return false
- }
- }
-
- return hasOverlap
- }
-
- Function("authorizationStatus") {
- let currentStatus = AuthorizationCenter.shared.authorizationStatus
-
- return currentStatus.rawValue
- }
-
- AsyncFunction("startMonitoring") { (activityName: String, schedule: ScheduleFromJS, events: [DeviceActivityEventFromJS], familyActivitySelections: [String]) in
- let schedule = DeviceActivitySchedule(
- intervalStart: convertToSwiftDateComponents(from: schedule.intervalStart),
- intervalEnd: convertToSwiftDateComponents(from: schedule.intervalEnd),
- repeats: schedule.repeats ?? false,
- warningTime: schedule.warningTime != nil
- ? convertToSwiftDateComponents(from: schedule.warningTime!)
- : nil
- )
-
- let decodedFamilyActivitySelections = familyActivitySelections.map { familyActivitySelection in
- let decoder = JSONDecoder()
- let data = Data(base64Encoded: familyActivitySelection)
- do {
- let activitySelection = try decoder.decode(FamilyActivitySelection.self, from: data!)
- return activitySelection
+
+ return filteredDict
+ }
+
+ Function("doesSelectionHaveOverlap") { (familyActivitySelections: [String]) in
+ let decodedFamilyActivitySelections: [FamilyActivitySelection] = familyActivitySelections.map { familyActivitySelection in
+ let decoder = JSONDecoder()
+ let data = Data(base64Encoded: familyActivitySelection)
+ do {
+ let activitySelection = try decoder.decode(FamilyActivitySelection.self, from: data!)
+ return activitySelection
+ }
+ catch {
+ return FamilyActivitySelection()
+ }
+ }
+
+ let hasOverlap = decodedFamilyActivitySelections.contains { selection in
+ return decodedFamilyActivitySelections.contains { compareWith in
+ // if it's the same instance - skip comparison
+ if(compareWith == selection){
+ return false
+ }
+
+ if(compareWith.applicationTokens.contains(where: { token in
+ return selection.applicationTokens.contains(token)
+ } )){
+ return true
+ }
+
+ if(compareWith.categoryTokens.contains(where: { token in
+ return selection.categoryTokens.contains(token)
+ } )){
+ return true
+ }
+
+ if(compareWith.webDomainTokens.contains(where: { token in
+ return selection.webDomainTokens.contains(token)
+ } )){
+ return true
+ }
+
+ return false
+ }
+ }
+
+ return hasOverlap
}
- catch {
- return FamilyActivitySelection()
+
+ Function("authorizationStatus") {
+ let currentStatus = AuthorizationCenter.shared.authorizationStatus
+
+ return currentStatus.rawValue
}
- }
-
- let dictionary = Dictionary(uniqueKeysWithValues: events.map { (eventRaw: DeviceActivityEventFromJS) in
- let familyActivitySelection = decodedFamilyActivitySelections[eventRaw.familyActivitySelectionIndex]
-
- userDefaults?.set(familyActivitySelections[eventRaw.familyActivitySelectionIndex], forKey: eventRaw.eventName + "_familyActivitySelection")
+
+ AsyncFunction("startMonitoring") { (activityName: String, schedule: ScheduleFromJS, events: [DeviceActivityEventFromJS], familyActivitySelections: [String]) in
+ let schedule = DeviceActivitySchedule(
+ intervalStart: convertToSwiftDateComponents(from: schedule.intervalStart),
+ intervalEnd: convertToSwiftDateComponents(from: schedule.intervalEnd),
+ repeats: schedule.repeats ?? false,
+ warningTime: schedule.warningTime != nil
+ ? convertToSwiftDateComponents(from: schedule.warningTime!)
+ : nil
+ )
+
+ let decodedFamilyActivitySelections = familyActivitySelections.map { familyActivitySelection in
+ let decoder = JSONDecoder()
+ let data = Data(base64Encoded: familyActivitySelection)
+ do {
+ let activitySelection = try decoder.decode(FamilyActivitySelection.self, from: data!)
+ return activitySelection
+ }
+ catch {
+ return FamilyActivitySelection()
+ }
+ }
- let threshold = convertToSwiftDateComponents(from: eventRaw.threshold)
- var event: DeviceActivityEvent
-
- if #available(iOS 17.4, *) {
- event = DeviceActivityEvent(
- applications: familyActivitySelection.applicationTokens,
- categories: familyActivitySelection.categoryTokens,
- webDomains: familyActivitySelection.webDomainTokens,
- threshold: threshold,
- includesPastActivity: eventRaw.includesPastActivity ?? false
- )
- } else {
-
-
- event = DeviceActivityEvent(
- applications: familyActivitySelection.applicationTokens,
- categories: familyActivitySelection.categoryTokens,
- webDomains: familyActivitySelection.webDomainTokens,
- threshold: threshold
- )
- }
+ let dictionary = Dictionary(uniqueKeysWithValues: events.map { (eventRaw: DeviceActivityEventFromJS) in
+ let familyActivitySelection = decodedFamilyActivitySelections[eventRaw.familyActivitySelectionIndex]
+
+ userDefaults?.set(familyActivitySelections[eventRaw.familyActivitySelectionIndex], forKey: eventRaw.eventName + "_familyActivitySelection")
+
+ let threshold = convertToSwiftDateComponents(from: eventRaw.threshold)
+ var event: DeviceActivityEvent
+
+ if #available(iOS 17.4, *) {
+ event = DeviceActivityEvent(
+ applications: familyActivitySelection.applicationTokens,
+ categories: familyActivitySelection.categoryTokens,
+ webDomains: familyActivitySelection.webDomainTokens,
+ threshold: threshold,
+ includesPastActivity: eventRaw.includesPastActivity ?? false
+ )
+ } else {
+
+
+ event = DeviceActivityEvent(
+ applications: familyActivitySelection.applicationTokens,
+ categories: familyActivitySelection.categoryTokens,
+ webDomains: familyActivitySelection.webDomainTokens,
+ threshold: threshold
+ )
+ }
+
+ return (
+ DeviceActivityEvent.Name(eventRaw.eventName),
+ event
+ )
+ })
+
+ do {
+ let activityName = DeviceActivityName(activityName)
+
+ try center.startMonitoring(
+ activityName,
+ during: schedule,
+ events: dictionary
+ )
+ logger.log("✅ Succeeded with Starting Monitor Activity: \(activityName.rawValue)")
+ } catch {
+ logger.log("❌ Failed with Starting Monitor Activity: \(error.localizedDescription)")
+ }
+ }
- return (
- DeviceActivityEvent.Name(eventRaw.eventName),
- event
- )
- })
-
- do {
- let activityName = DeviceActivityName(activityName)
-
- try center.startMonitoring(
- activityName,
- during: schedule,
- events: dictionary
- )
- logger.log("✅ Succeeded with Starting Monitor Activity: \(activityName.rawValue)")
- } catch {
- logger.log("❌ Failed with Starting Monitor Activity: \(error.localizedDescription)")
- }
- }
-
- Function("stopMonitoring") { (activityNames: [String]?) in
- if(activityNames == nil || activityNames?.count == 0){
- center.stopMonitoring()
- return
- }
- center.stopMonitoring(activityNames!.map({ activityName in
- return DeviceActivityName(activityName)
- }))
- }
-
- let store = ManagedSettingsStore()
-
- Function("updateShieldConfiguration") { (shieldConfiguration: [String:Any]) -> Void in
- logger.log("\(shieldConfiguration)")
- userDefaults?.set(shieldConfiguration, forKey: "shieldConfiguration")
-
- }
-
- Function("activities") {
- let activities = center.activities
-
- return activities.map { activity in
- return activity.rawValue
+ Function("stopMonitoring") { (activityNames: [String]?) in
+ if(activityNames == nil || activityNames?.count == 0){
+ center.stopMonitoring()
+ return
+ }
+ center.stopMonitoring(activityNames!.map({ activityName in
+ return DeviceActivityName(activityName)
+ }))
}
- }
-
- AsyncFunction("requestAuthorization"){
- let ac = AuthorizationCenter.shared
-
- if #available(iOS 16.0, *) {
- try await ac.requestAuthorization(for: .individual)
- } else {
- logger.log("⚠️ iOS 16.0 or later is required to request authorization.")
- }
-
- }
-
- Function("blockAllApps"){
- store.shield.applicationCategories = ShieldSettings.ActivityCategoryPolicy.all(except: Set())
- store.shield.webDomainCategories = ShieldSettings.ActivityCategoryPolicy.all(except: Set())
- }
-
- Function("unblockApps"){
- store.shield.applicationCategories = nil
- store.shield.webDomainCategories = nil
- }
-
- AsyncFunction("revokeAuthorization") { () async throws -> Void in
- let ac = AuthorizationCenter.shared
- return try await withCheckedThrowingContinuation { continuation in
- ac.revokeAuthorization { result in
- switch result {
- case .success:
- continuation.resume()
- case .failure(let error):
- logger.log("❌ Failed to revoke authorization: \(error.localizedDescription)")
- continuation.resume(throwing: error)
+ let store = ManagedSettingsStore()
+
+ Function("updateShieldConfiguration") { (shieldConfiguration: [String:Any]) -> Void in
+ logger.log("\(shieldConfiguration)")
+ userDefaults?.set(shieldConfiguration, forKey: "shieldConfiguration")
+
+ }
+
+ Function("activities") {
+ let activities = center.activities
+
+ return activities.map { activity in
+ return activity.rawValue
}
- }
}
- }
-
- Events(
- "onSelectionChange",
- "onDeviceActivityMonitorEvent"
- )
-
- // Enables the module to be used as a native view. Definition components that are accepted as part of the
- // view definition: Prop, Events.
- View(ReactNativeDeviceActivityView.self) {
- Events(
- "onSelectionChange"
- )
- // Defines a setter for the `name` prop.
- Prop("familyActivitySelection") { (view: ReactNativeDeviceActivityView, prop: String) in
- do {
- let decoder = JSONDecoder()
- let data = Data(base64Encoded: prop)!
- let selection = try decoder.decode(FamilyActivitySelection.self, from: data)
-
- view.model.activitySelection = selection
- } catch {
- logger.log("❌ Failed to deserialize familyActivitySelection to FamilyActivitySelection: \(error.localizedDescription)")
+
+ AsyncFunction("requestAuthorization"){
+ let ac = AuthorizationCenter.shared
+
+ if #available(iOS 16.0, *) {
+ try await ac.requestAuthorization(for: .individual)
+ } else {
+ logger.log("⚠️ iOS 16.0 or later is required to request authorization.")
+ }
+
}
- }
- Prop("footerText") { (view: ReactNativeDeviceActivityView, prop: String?) in
-
- view.model.footerText = prop
-
+ Function("blockAllApps"){
+ store.shield.applicationCategories = ShieldSettings.ActivityCategoryPolicy.all(except: Set())
+ store.shield.webDomainCategories = ShieldSettings.ActivityCategoryPolicy.all(except: Set())
}
- Prop("headerText") { (view: ReactNativeDeviceActivityView, prop: String?) in
-
- view.model.headerText = prop
-
+ Function("unblockApps"){
+ store.shield.applicationCategories = nil
+ store.shield.webDomainCategories = nil
+ }
+
+ AsyncFunction("revokeAuthorization") { () async throws -> Void in
+ let ac = AuthorizationCenter.shared
+
+ return try await withCheckedThrowingContinuation { continuation in
+ ac.revokeAuthorization { result in
+ switch result {
+ case .success:
+ continuation.resume()
+ case .failure(let error):
+ logger.log("❌ Failed to revoke authorization: \(error.localizedDescription)")
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
+ Events(
+ "onSelectionChange",
+ "onDeviceActivityMonitorEvent"
+ )
+
+ // Enables the module to be used as a native view. Definition components that are accepted as part of the
+ // view definition: Prop, Events.
+ View(ReactNativeDeviceActivityView.self) {
+ Events(
+ "onSelectionChange"
+ )
+ // Defines a setter for the `name` prop.
+ Prop("familyActivitySelection") { (view: ReactNativeDeviceActivityView, prop: String) in
+ do {
+ let decoder = JSONDecoder()
+ let data = Data(base64Encoded: prop)!
+ let selection = try decoder.decode(FamilyActivitySelection.self, from: data)
+
+ view.model.activitySelection = selection
+ } catch {
+ logger.log("❌ Failed to deserialize familyActivitySelection to FamilyActivitySelection: \(error.localizedDescription)")
+ }
+ }
+
+ Prop("footerText") { (view: ReactNativeDeviceActivityView, prop: String?) in
+
+ view.model.footerText = prop
+
+ }
+
+ Prop("headerText") { (view: ReactNativeDeviceActivityView, prop: String?) in
+
+ view.model.headerText = prop
+
+ }
+ }
+
+
+ View(DeviceActivityReportView.self) { // Defines a setter for the `name` prop.
+ Events()
+ Prop("familyActivitySelection") { (view: DeviceActivityReportView, prop: String) in
+ do {
+ let decoder = JSONDecoder()
+ let data = Data(base64Encoded: prop)!
+ let selection = try decoder.decode(FamilyActivitySelection.self, from: data)
+
+ view.model.familyActivitySelection = selection
+ } catch {
+ logger.log("❌ Failed to deserialize familyActivitySelection to FamilyActivitySelection: \(error.localizedDescription)")
+ }
+ }
+
+ Prop("context") { (view: DeviceActivityReportView, prop: String) in
+ view.model.context = prop
+ }
+
+ Prop("from") { (view: DeviceActivityReportView, prop: Double?) in
+ view.model.from = prop != nil ? Date(timeIntervalSince1970: .init(floatLiteral: prop!)) : .distantPast
+ }
+
+ Prop("to") { (view: DeviceActivityReportView, prop: Double?) in
+ view.model.to = prop != nil ? Date(timeIntervalSince1970: .init(floatLiteral: prop!)) : .distantFuture
+ }
+
+ Prop("segmentation") { (view: DeviceActivityReportView, prop: String) in
+ view.model.segmentation = prop
+ }
+
+ Prop("devices") { (view: DeviceActivityReportView, prop: [Int]?) in
+ view.model.devices = prop == nil || prop?.count == 0 ? .all : .init(Set(prop!.map({ model in
+ return DeviceActivityData.Device.Model(rawValue: model)!
+ })))
+ }
+
+ Prop("users") { (view: DeviceActivityReportView, prop: String?) in
+ view.model.users = prop == "children" ? .children : prop == "all" ? .all : nil
+ }
}
- }
}
}
diff --git a/ios/ReactNativeDeviceActivityView.swift b/ios/ReactNativeDeviceActivityView.swift
index 5842babe..beb3c080 100644
--- a/ios/ReactNativeDeviceActivityView.swift
+++ b/ios/ReactNativeDeviceActivityView.swift
@@ -6,7 +6,7 @@ import Combine
// This view will be used as a native component. Make sure to inherit from `ExpoView`
// to apply the proper styling (e.g. border radius and shadows).
-@available(iOS 15.0, *)
+@available(iOS 16.0, *)
class ReactNativeDeviceActivityView: ExpoView {
public let model = ScreenTimeSelectAppsModel()
diff --git a/ios/ScreenTimeActivityPicker.swift b/ios/ScreenTimeActivityPicker.swift
index 96349c8c..d2f30a1b 100644
--- a/ios/ScreenTimeActivityPicker.swift
+++ b/ios/ScreenTimeActivityPicker.swift
@@ -7,6 +7,7 @@
import Foundation
import FamilyControls
+import DeviceActivity
import SwiftUI
@available(iOS 15.0, *)
@@ -29,7 +30,7 @@ struct InnerView: View {
}
-@available(iOS 15.0, *)
+@available(iOS 16.0, *)
struct ScreenTimeSelectAppsContentView: View {
@State private var pickerIsPresented = false
@ObservedObject var model: ScreenTimeSelectAppsModel
diff --git a/src/DeviceActivityReport.ios.tsx b/src/DeviceActivityReport.ios.tsx
new file mode 100644
index 00000000..a325d4a7
--- /dev/null
+++ b/src/DeviceActivityReport.ios.tsx
@@ -0,0 +1,19 @@
+import { requireNativeViewManager } from "expo-modules-core";
+import * as React from "react";
+
+import { DeviceActivityReportViewProps } from "./ReactNativeDeviceActivity.types";
+
+const NativeView: React.ComponentType =
+ requireNativeViewManager("DeviceActivityReportView");
+
+export default function DeviceActivityReportView({
+ style,
+ children,
+ ...props
+}: DeviceActivityReportViewProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/DeviceActivityReport.tsx b/src/DeviceActivityReport.tsx
new file mode 100644
index 00000000..3bd0189b
--- /dev/null
+++ b/src/DeviceActivityReport.tsx
@@ -0,0 +1,12 @@
+import * as React from "react";
+import { View } from "react-native";
+
+import { DeviceActivityReportViewProps } from "./ReactNativeDeviceActivity.types";
+
+export default function DeviceActivityReportView({
+ style,
+ children,
+ ...props
+}: DeviceActivityReportViewProps) {
+ return {children};
+}
diff --git a/src/ReactNativeDeviceActivity.types.ts b/src/ReactNativeDeviceActivity.types.ts
index 832f9cd3..4b1ce18c 100644
--- a/src/ReactNativeDeviceActivity.types.ts
+++ b/src/ReactNativeDeviceActivity.types.ts
@@ -21,6 +21,25 @@ export type EventParsed = {
export type EventsLookup = Record;
+export enum DeviceActivityReportViewDevice {
+ iPad = 0,
+ iPhone = 1,
+ iPod = 2,
+ mac = 3,
+}
+
+export type DeviceActivityReportViewProps = PropsWithChildren<{
+ style: StyleProp;
+ familyActivitySelection?: string | null;
+ from?: number | null;
+ to?: number | null;
+ segmentation?: "hourly" | "daily" | "weekly";
+ // null to select all devices
+ devices?: DeviceActivityReportViewDevice[] | null;
+ // null to select current user
+ users?: "all" | "children" | null;
+}>;
+
export type DeviceActivitySelectionViewProps = PropsWithChildren<{
style: StyleProp;
onSelectionChange?: (
@@ -131,6 +150,10 @@ export type DeviceActivityEvent = {
familyActivitySelection: FamilyActivitySelection;
threshold: DateComponents;
eventName: string;
+ /**
+ * @link https://developer.apple.com/documentation/deviceactivity/deviceactivityevent/includespastactivity
+ */
+ includesPastActivity?: boolean;
};
export type DeviceActivityEventRaw = Omit<
diff --git a/src/index.ts b/src/index.ts
index 6bf0bb35..0780f0e1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ import {
// and on native platforms to ReactNativeDeviceActivity.ts
import { Platform } from "react-native";
+import DeviceActivityReportView from "./DeviceActivityReport";
import DeviceActivitySelectionView from "./DeviceActivitySelectionView";
import {
AuthorizationStatus,
@@ -155,5 +156,6 @@ export function isAvailable(): boolean {
export {
DeviceActivitySelectionView,
+ DeviceActivityReportView,
DeviceActivitySelectionViewProps as ReactNativeDeviceActivityViewProps,
};