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, };