diff --git a/README.md b/README.md index c3fe88ef..b67d4c35 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,59 @@ ReactNativeDeviceActivity.revokeAuthorization(); ### Select Apps to track -For most use cases you need to get an activitySelection from the user, which is a token representing the apps the user wants to track, block or whitelist. This can be done by presenting the native view: +For most use cases you need to get an activitySelection from the user, which is a token representing the apps the user wants to track, block or whitelist. This can be done by presenting the native `DeviceActivitySelectionView`. + +#### Presentation options + +The picker now has dedicated components for each presentation style: + +`*SelectionView` components take a raw `familyActivitySelection` token. +`*SelectionViewPersisted` components take a `familyActivitySelectionId` and persist/read the token on the native side by ID. + +**Native sheet** -- `DeviceActivitySelectionSheetView` (and persisted variant) uses Apple's `.familyActivityPicker(isPresented:selection:)` flow with native Cancel/Done controls. + +```TypeScript +// The sheet view acts as an invisible anchor. +// The native side presents the iOS sheet and fires onDismissRequest on Cancel/Done. +{pickerVisible && ( + setPickerVisible(false)} + onSelectionChange={handleSelectionChange} + familyActivitySelection={familyActivitySelection} + /> +)} +``` + +**Custom presentation (fallback/customizable)** -- `DeviceActivitySelectionView` (and persisted variant) renders inline. You can embed it directly in your layout or wrap it in a React Native `` for a custom sheet. + +```TypeScript +import { Modal, View } from "react-native"; + + + + + + +``` + +#### Which one should I use? + +- Use `DeviceActivitySelectionSheetView` for a native iOS sheet UX (system Cancel/Done). +- Use `DeviceActivitySelectionView` when you need full control over presentation and a custom crash fallback UI. +- Use the persisted variants when you want to store/reuse selections across screens/sessions or avoid passing very large selection tokens through JS. + +#### Full example ```TypeScript import * as ReactNativeDeviceActivity from "react-native-device-activity"; @@ -550,7 +602,10 @@ For a complete implementation, see the [example app](https://github.com/Kingstin | Component | Props | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| `DeviceActivitySelectionView` | `familyActivitySelection`: string \| null
`onSelectionChange`: (event) => void
`style`: ViewStyle | Native component that renders the app selection UI | +| `DeviceActivitySelectionView` | `familyActivitySelection`: string \| null
`onSelectionChange`: (event) => void
`headerText?`: string
`footerText?`: string
`style`: ViewStyle | Inline/customizable native picker view. Useful when you want to control modal/presentation yourself and provide a fallback UI. | +| `DeviceActivitySelectionViewPersisted` | `familyActivitySelectionId`: string
`onSelectionChange`: (event) => void
`includeEntireCategory?`: boolean
`headerText?`: string
`footerText?`: string
`style`: ViewStyle | Persisted inline/customizable picker keyed by `familyActivitySelectionId`. | +| `DeviceActivitySelectionSheetView` | `familyActivitySelection`: string \| null
`onSelectionChange`: (event) => void
`headerText?`: string
`footerText?`: string
`onDismissRequest?`: (event) => void
`style`: ViewStyle | Dedicated native iOS sheet picker with Cancel/Done controls. | +| `DeviceActivitySelectionSheetViewPersisted` | `familyActivitySelectionId`: string
`onSelectionChange`: (event) => void
`includeEntireCategory?`: boolean
`headerText?`: string
`footerText?`: string
`onDismissRequest?`: (event) => void
`style`: ViewStyle | Persisted dedicated native iOS sheet picker keyed by `familyActivitySelectionId`. | ### Hooks diff --git a/apps/example/components/ActivityPicker.tsx b/apps/example/components/ActivityPicker.tsx index 15bbf251..c479354b 100644 --- a/apps/example/components/ActivityPicker.tsx +++ b/apps/example/components/ActivityPicker.tsx @@ -1,6 +1,9 @@ -import { Pressable, Text, View, NativeSyntheticEvent } from "react-native"; +import React from "react"; +import { NativeSyntheticEvent, Pressable, StyleSheet, Text, View } from "react-native"; import { ActivitySelectionMetadata, + DeviceActivitySelectionSheetView, + DeviceActivitySelectionSheetViewPersisted, ActivitySelectionWithMetadata, DeviceActivitySelectionView, DeviceActivitySelectionViewPersisted, @@ -10,15 +13,7 @@ import { Modal, Portal } from "react-native-paper"; const CrashView = ({ onReload }: { onReload: () => void }) => { return ( Swift view crash - tap to reload @@ -32,6 +27,7 @@ export const ActivityPicker = ({ onSelectionChange, familyActivitySelection, onReload, + showNavigationBar = true, }: { visible: boolean; onDismiss: () => void; @@ -40,35 +36,36 @@ export const ActivityPicker = ({ ) => void; familyActivitySelection: string | undefined; onReload: () => void; + showNavigationBar?: boolean; }) => { + if (showNavigationBar) { + // Native presentation: the native side uses the + // .familyActivityPicker(isPresented:) modifier which presents its own + // sheet. We just mount a tiny anchor view — no RN Modal needed. + if (!visible) return null; + return ( + + ); + } + + // Custom modal: react-native-paper Portal + Modal with fixed height. return ( - + - {visible && ( @@ -86,6 +83,7 @@ export const ActivityPickerPersisted = ({ familyActivitySelectionId, onReload, includeEntireCategory, + showNavigationBar = true, }: { visible: boolean; onDismiss: () => void; @@ -96,35 +94,33 @@ export const ActivityPickerPersisted = ({ familyActivitySelectionId: string; onReload: () => void; includeEntireCategory?: boolean; + showNavigationBar?: boolean; }) => { + if (showNavigationBar) { + if (!visible) return null; + return ( + + ); + } + return ( - + - {visible && ( ); }; + +const styles = StyleSheet.create({ + // Invisible anchor for the native .familyActivityPicker() modifier. + nativeAnchor: { + width: 1, + height: 1, + position: "absolute", + }, + modalContainer: { + height: 600, + }, + modalContent: { + flex: 1, + height: 600, + }, + crashView: { + flex: 1, + position: "absolute", + height: 600, + width: "100%", + alignItems: "center", + justifyContent: "center", + backgroundColor: "white", + }, + picker: { + flex: 1, + height: 600, + width: "100%", + backgroundColor: "transparent", + }, +}); diff --git a/apps/example/components/CreateActivity.tsx b/apps/example/components/CreateActivity.tsx index a91e70ec..8488c3b6 100644 --- a/apps/example/components/CreateActivity.tsx +++ b/apps/example/components/CreateActivity.tsx @@ -1,6 +1,6 @@ import { requestPermissionsAsync } from "expo-notifications"; import { useCallback, useState } from "react"; -import { NativeSyntheticEvent, View } from "react-native"; +import { NativeSyntheticEvent, Pressable, View } from "react-native"; import * as ReactNativeDeviceActivity from "react-native-device-activity"; import { DeviceActivityEvent, @@ -8,6 +8,8 @@ import { } from "react-native-device-activity/src/ReactNativeDeviceActivity.types"; import { Button, Text, TextInput, Title, useTheme } from "react-native-paper"; +import { ActivityPicker } from "./ActivityPicker"; + const trackEveryXMinutes = 10; const potentialMaxEvents = Math.floor((60 * 24) / trackEveryXMinutes); @@ -175,6 +177,7 @@ export const CreateActivity = ({ onDismiss }: { onDismiss: () => void }) => { ); const [activityName, setActivityName] = useState(""); + const [showSelectionView, setShowSelectionView] = useState(false); return ( @@ -187,33 +190,19 @@ export const CreateActivity = ({ onDismiss }: { onDismiss: () => void }) => { marginVertical: 10, }} > - setShowSelectionView(true)} > - - Select apps - - + Select apps + {familyActivitySelectionResult && familyActivitySelectionResult?.categoryCount < 13 @@ -223,6 +212,20 @@ export const CreateActivity = ({ onDismiss }: { onDismiss: () => void }) => { : "Nothing selected"} + setShowSelectionView(false)} + onSelectionChange={onSelectionChange} + familyActivitySelection={ + familyActivitySelectionResult?.familyActivitySelection ?? undefined + } + onReload={() => { + setShowSelectionView(false); + setTimeout(() => { + setShowSelectionView(true); + }, 100); + }} + /> setActivityName(text)} diff --git a/apps/example/screens/SimpleTab.tsx b/apps/example/screens/SimpleTab.tsx index 278a68f3..82a5616d 100644 --- a/apps/example/screens/SimpleTab.tsx +++ b/apps/example/screens/SimpleTab.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { + NativeSyntheticEvent, ScrollView, StyleSheet, Linking, @@ -18,9 +19,11 @@ import { useAuthorizationStatus, AuthorizationStatusType, requestAuthorization, + ActivitySelectionMetadata, } from "react-native-device-activity"; import { Button, Modal, Text, Title } from "react-native-paper"; +import { ActivityPickerPersisted } from "../components/ActivityPicker"; import { CreateActivity } from "../components/CreateActivity"; const authorizationStatusMap: Record = { @@ -59,6 +62,9 @@ export function SimpleTab() { const [showCreateActivityPopup, setShowCreateActivityPopup] = useState(false); + const [pickerNative, setPickerNative] = useState(false); + const [pickerCustomModal, setPickerCustomModal] = useState(false); + const [refreshing, setRefreshing] = useState(false); useEffect(() => { @@ -140,6 +146,22 @@ export function SimpleTab() { > Create Activity + + Picker Views + + + setPickerNative(false)} + showNavigationBar + onSelectionChange={( + event: NativeSyntheticEvent, + ) => { + console.log("sheet view selection changed", event.nativeEvent); + }} + familyActivitySelectionId="picker-native" + onReload={() => { + setPickerNative(false); + setTimeout(() => setPickerNative(true), 100); + }} + /> + setPickerCustomModal(false)} + showNavigationBar={false} + onSelectionChange={( + event: NativeSyntheticEvent, + ) => { + console.log("selection view changed", event.nativeEvent); + }} + familyActivitySelectionId="picker-custom-modal" + onReload={() => { + setPickerCustomModal(false); + setTimeout(() => setPickerCustomModal(true), 100); + }} + /> ); } diff --git a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityModule.swift b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityModule.swift index 3fe03eb0..c1ccf0fc 100644 --- a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityModule.swift +++ b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityModule.swift @@ -213,7 +213,8 @@ class NativeEventObserver { _: CFDictionary? ) in if let observer = observer, let name = name { - let nativeObserver = Unmanaged.fromOpaque(observer).takeUnretainedValue() + let nativeObserver = Unmanaged.fromOpaque(observer) + .takeUnretainedValue() guard let module = nativeObserver.module else { return } @@ -828,7 +829,8 @@ public class ReactNativeDeviceActivityModule: Module { // view definition: Prop, Events. View(ReactNativeDeviceActivityView.self) { Events( - "onSelectionChange" + "onSelectionChange", + "onDismissRequest" ) // Defines a setter for the `name` prop. Prop("familyActivitySelection") { (view: ReactNativeDeviceActivityView, prop: String) in @@ -844,7 +846,18 @@ public class ReactNativeDeviceActivityModule: Module { Prop("headerText") { (view: ReactNativeDeviceActivityView, prop: String?) in view.model.headerText = prop } + + Prop("showNavigationBar") { (view: ReactNativeDeviceActivityView, prop: Bool?) in + let enabled = prop ?? false + view.model.showNavigationBar = enabled + // When using the native .familyActivityPicker() modifier, set the + // hosting controller background so the presented sheet inherits it + // instead of falling through to the window's white default. + view.contentView.view.backgroundColor = enabled ? .systemGroupedBackground : .clear + view.backgroundColor = enabled ? .systemGroupedBackground : .clear + } } + } } @@ -854,7 +867,8 @@ public class ReactNativeDeviceActivityViewPersistedModule: Module { Name("ReactNativeDeviceActivityViewPersistedModule") View(ReactNativeDeviceActivityViewPersisted.self) { Events( - "onSelectionChange" + "onSelectionChange", + "onDismissRequest" ) // Defines a setter for the `name` prop. Prop("familyActivitySelectionId") { @@ -894,6 +908,13 @@ public class ReactNativeDeviceActivityViewPersistedModule: Module { Prop("headerText") { (view: ReactNativeDeviceActivityViewPersisted, prop: String?) in view.model.headerText = prop } + + Prop("showNavigationBar") { (view: ReactNativeDeviceActivityViewPersisted, prop: Bool?) in + let enabled = prop ?? false + view.model.showNavigationBar = enabled + view.contentView.view.backgroundColor = enabled ? .systemGroupedBackground : .clear + view.backgroundColor = enabled ? .systemGroupedBackground : .clear + } } } } diff --git a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityView.swift b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityView.swift index 675dfcf2..897a3221 100644 --- a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityView.swift +++ b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityView.swift @@ -31,6 +31,10 @@ class ReactNativeDeviceActivityView: ExpoView { self.addSubview(contentView.view) + model.onDismissRequest = { [weak self] in + self?.onDismissRequest([:]) + } + model.$activitySelection.debounce(for: .seconds(0.1), scheduler: RunLoop.main).sink { selection in if selection != self.previousSelection { @@ -45,7 +49,37 @@ class ReactNativeDeviceActivityView: ExpoView { contentView.view.frame = bounds } + override func didMoveToWindow() { + super.didMoveToWindow() + if window != nil { + // Establish a proper UIKit parent–child VC relationship so that + // SwiftUI presentation modifiers (like .familyActivityPicker) can + // walk the VC hierarchy and present sheets. + if contentView.parent == nil, let parentVC = parentViewController { + parentVC.addChild(contentView) + contentView.didMove(toParent: parentVC) + } + } else { + if contentView.parent != nil { + contentView.willMove(toParent: nil) + contentView.removeFromParent() + } + } + } + + private var parentViewController: UIViewController? { + var responder: UIResponder? = self + while let next = responder?.next { + if let vc = next as? UIViewController { + return vc + } + responder = next + } + return nil + } + let onSelectionChange = EventDispatcher() + let onDismissRequest = EventDispatcher() var previousSelection: FamilyActivitySelection? diff --git a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityViewPersisted.swift b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityViewPersisted.swift index ea3cde08..f37a2ce0 100644 --- a/packages/react-native-device-activity/ios/ReactNativeDeviceActivityViewPersisted.swift +++ b/packages/react-native-device-activity/ios/ReactNativeDeviceActivityViewPersisted.swift @@ -31,6 +31,10 @@ class ReactNativeDeviceActivityViewPersisted: ExpoView { self.addSubview(contentView.view) + model.onDismissRequest = { [weak self] in + self?.onDismissRequest([:]) + } + model.$activitySelection.debounce(for: .seconds(0.1), scheduler: RunLoop.main).sink { selection in if selection != self.previousSelection { @@ -45,7 +49,34 @@ class ReactNativeDeviceActivityViewPersisted: ExpoView { contentView.view.frame = bounds } + override func didMoveToWindow() { + super.didMoveToWindow() + if window != nil { + if contentView.parent == nil, let parentVC = parentViewController { + parentVC.addChild(contentView) + contentView.didMove(toParent: parentVC) + } + } else { + if contentView.parent != nil { + contentView.willMove(toParent: nil) + contentView.removeFromParent() + } + } + } + + private var parentViewController: UIViewController? { + var responder: UIResponder? = self + while let next = responder?.next { + if let vc = next as? UIViewController { + return vc + } + responder = next + } + return nil + } + let onSelectionChange = EventDispatcher() + let onDismissRequest = EventDispatcher() var previousSelection: FamilyActivitySelection? diff --git a/packages/react-native-device-activity/ios/ScreenTimeActivityPicker.swift b/packages/react-native-device-activity/ios/ScreenTimeActivityPicker.swift index b175a1b4..7a82d7e9 100644 --- a/packages/react-native-device-activity/ios/ScreenTimeActivityPicker.swift +++ b/packages/react-native-device-activity/ios/ScreenTimeActivityPicker.swift @@ -23,6 +23,10 @@ class ScreenTimeSelectAppsModel: ObservableObject { @Published public var includeEntireCategory: Bool? + @Published public var showNavigationBar: Bool = false + + var onDismissRequest: (() -> Void)? + init() {} } @@ -30,21 +34,139 @@ class ScreenTimeSelectAppsModel: ObservableObject { struct ActivityPicker: View { @ObservedObject var model: ScreenTimeSelectAppsModel + /// Local state used by the `.familyActivityPicker` modifier to drive + /// its native sheet presentation. + @State private var isPickerPresented = false + + private var resolvedHeaderText: String? { + let trimmed = model.headerText?.trimmingCharacters(in: .whitespacesAndNewlines) + return (trimmed?.isEmpty == false) ? trimmed : nil + } + + private var resolvedFooterText: String? { + let trimmed = model.footerText?.trimmingCharacters(in: .whitespacesAndNewlines) + return (trimmed?.isEmpty == false) ? trimmed : nil + } + var body: some View { - if #available(iOS 16.0, *) { + if model.showNavigationBar { + // Use the `.familyActivityPicker(isPresented:selection:)` **modifier** + // instead of the inline `FamilyActivityPicker` view. The modifier + // presents the picker as a native sheet with Cancel/Done in the nav bar. + nativeSheetPresentation + } else { + pickerContent + } + } + + // MARK: - Native sheet (modifier-based) presentation + + @ViewBuilder + private var nativeSheetPresentation: some View { + if #available(iOS 16.0, *), resolvedHeaderText != nil || resolvedFooterText != nil { + Color.clear + .familyActivityPicker( + headerText: resolvedHeaderText, + footerText: resolvedFooterText, + isPresented: $isPickerPresented, + selection: $model.activitySelection + ) + .onAppear { isPickerPresented = true } + .onChange(of: isPickerPresented) { presented in + if !presented { model.onDismissRequest?() } + } + .background(PresentedSheetBackgroundFixer()) + } else { + Color.clear + .familyActivityPicker( + isPresented: $isPickerPresented, + selection: $model.activitySelection + ) + .onAppear { isPickerPresented = true } + .onChange(of: isPickerPresented) { presented in + if !presented { model.onDismissRequest?() } + } + .background(PresentedSheetBackgroundFixer()) + } + } + + // MARK: - Inline (embedded) picker + + @ViewBuilder + private var pickerContent: some View { + if #available(iOS 16.0, *), resolvedHeaderText != nil || resolvedFooterText != nil { FamilyActivityPicker( - headerText: model.headerText, - footerText: model.footerText, + headerText: resolvedHeaderText, + footerText: resolvedFooterText, selection: $model.activitySelection ) - .allowsHitTesting(false) - .background(Color.clear) } else { FamilyActivityPicker( selection: $model.activitySelection ) - .allowsHitTesting(false) - .background(Color.clear) + } + } +} + +// MARK: - Sheet background fix + +/// Finds the presented picker sheet's view hierarchy and sets the background +/// to `systemGroupedBackground` so the empty area below the list matches +/// the rest of the sheet. Uses a VC representable that observes when our +/// hosting controller presents a child. +@available(iOS 15.0, *) +struct PresentedSheetBackgroundFixer: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> SheetBackgroundFixerController { + SheetBackgroundFixerController() + } + + func updateUIViewController(_ uiViewController: SheetBackgroundFixerController, context: Context) {} +} + +@available(iOS 15.0, *) +class SheetBackgroundFixerController: UIViewController { + private var observation: NSKeyValueObservation? + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + startObserving() + } + + private func startObserving() { + // Walk up to find the VC that will present the picker sheet. + var candidate: UIViewController? = self + while let c = candidate { + // Observe `presentedViewController` so we catch it the moment the + // picker sheet appears. + observation = c.observe( + \.presentedViewController, options: [.new] + ) { [weak self] vc, _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + self?.fixPresentedBackground(from: vc) + } + } + // Also try immediately in case it's already presented. + fixPresentedBackground(from: c) + if c.presentedViewController != nil || c.parent == nil { break } + candidate = c.parent + } + } + + private func fixPresentedBackground(from vc: UIViewController) { + guard let presented = vc.presentedViewController else { return } + applySystemBackground(to: presented.view) + for child in presented.children { + applySystemBackground(to: child.view) + } + } + + private func applySystemBackground(to view: UIView) { + view.backgroundColor = .systemGroupedBackground + // The picker nests views; walk a few levels deep. + for sub in view.subviews { + if sub.backgroundColor == .white || sub.backgroundColor == .systemBackground { + sub.backgroundColor = .systemGroupedBackground + } } } } diff --git a/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.ios.tsx b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.ios.tsx new file mode 100644 index 00000000..79f6d676 --- /dev/null +++ b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.ios.tsx @@ -0,0 +1,21 @@ +import { requireNativeViewManager } from "expo-modules-core"; +import * as React from "react"; + +import { + DeviceActivitySelectionSheetViewProps, + DeviceActivitySelectionViewProps, +} from "./ReactNativeDeviceActivity.types"; + +type NativeSheetViewProps = DeviceActivitySelectionViewProps & { + showNavigationBar: boolean; + onDismissRequest?: DeviceActivitySelectionSheetViewProps["onDismissRequest"]; +}; + +const NativeView: React.ComponentType = + requireNativeViewManager("ReactNativeDeviceActivity"); + +export default function DeviceActivitySelectionSheetView( + props: DeviceActivitySelectionSheetViewProps, +) { + return ; +} diff --git a/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.tsx b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.tsx new file mode 100644 index 00000000..694d8b41 --- /dev/null +++ b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetView.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { View } from "react-native"; + +import { DeviceActivitySelectionSheetViewProps } from "./ReactNativeDeviceActivity.types"; + +export default function DeviceActivitySelectionSheetView({ + style, + children, +}: DeviceActivitySelectionSheetViewProps) { + return {children}; +} diff --git a/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.ios.tsx b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.ios.tsx new file mode 100644 index 00000000..5a4c0947 --- /dev/null +++ b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.ios.tsx @@ -0,0 +1,22 @@ +import { requireNativeViewManager } from "expo-modules-core"; +import * as React from "react"; + +import { + DeviceActivitySelectionSheetViewPersistedProps, + DeviceActivitySelectionViewPersistedProps, +} from "./ReactNativeDeviceActivity.types"; + +type NativeSheetViewPersistedProps = DeviceActivitySelectionViewPersistedProps & { + showNavigationBar: boolean; + onDismissRequest?: + DeviceActivitySelectionSheetViewPersistedProps["onDismissRequest"]; +}; + +const NativeView: React.ComponentType = + requireNativeViewManager("ReactNativeDeviceActivityViewPersistedModule"); + +export default function DeviceActivitySelectionSheetViewPersisted( + props: DeviceActivitySelectionSheetViewPersistedProps, +) { + return ; +} diff --git a/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.tsx b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.tsx new file mode 100644 index 00000000..6d560269 --- /dev/null +++ b/packages/react-native-device-activity/src/DeviceActivitySelectionSheetViewPersisted.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; +import { View } from "react-native"; + +import { DeviceActivitySelectionSheetViewPersistedProps } from "./ReactNativeDeviceActivity.types"; + +export default function DeviceActivitySelectionSheetViewPersisted({ + style, + children, +}: DeviceActivitySelectionSheetViewPersistedProps) { + return {children}; +} diff --git a/packages/react-native-device-activity/src/ReactNativeDeviceActivity.types.ts b/packages/react-native-device-activity/src/ReactNativeDeviceActivity.types.ts index 1fae7904..5ae9bcf4 100644 --- a/packages/react-native-device-activity/src/ReactNativeDeviceActivity.types.ts +++ b/packages/react-native-device-activity/src/ReactNativeDeviceActivity.types.ts @@ -46,6 +46,13 @@ export type DeviceActivitySelectionViewProps = PropsWithChildren<{ footerText?: string | null; }>; +export type DeviceActivitySelectionSheetViewProps = DeviceActivitySelectionViewProps & { + /** + * Called when the user taps Cancel or Done in the native sheet navigation bar. + */ + onDismissRequest?: (event: NativeSyntheticEvent>) => void; +}; + export type DeviceActivitySelectionViewPersistedProps = PropsWithChildren<{ style?: StyleProp; onSelectionChange?: ( @@ -61,6 +68,14 @@ export type DeviceActivitySelectionViewPersistedProps = PropsWithChildren<{ includeEntireCategory?: boolean; }>; +export type DeviceActivitySelectionSheetViewPersistedProps = + DeviceActivitySelectionViewPersistedProps & { + /** + * Called when the user taps Cancel or Done in the native sheet navigation bar. + */ + onDismissRequest?: (event: NativeSyntheticEvent>) => void; + }; + /** * @link https://developer.apple.com/documentation/foundation/datecomponents */ diff --git a/packages/react-native-device-activity/src/index.test.ts b/packages/react-native-device-activity/src/index.test.ts index db8d511c..b32e1b29 100644 --- a/packages/react-native-device-activity/src/index.test.ts +++ b/packages/react-native-device-activity/src/index.test.ts @@ -1,6 +1,14 @@ // todo: skipping for now describe("test", () => { + test("Should export sheet picker views", () => { + jest.isolateModules(() => { + const module = require("./"); + expect(module.DeviceActivitySelectionSheetView).toBeDefined(); + expect(module.DeviceActivitySelectionSheetViewPersisted).toBeDefined(); + }); + }); + test("Should call stopMonitoring", () => { const mockStopMonitoring = jest.fn(); jest.mock("./ReactNativeDeviceActivityModule", () => ({ diff --git a/packages/react-native-device-activity/src/index.ts b/packages/react-native-device-activity/src/index.ts index 938f02da..0e01eaaa 100644 --- a/packages/react-native-device-activity/src/index.ts +++ b/packages/react-native-device-activity/src/index.ts @@ -2,6 +2,8 @@ import { EventEmitter, EventSubscription } from "expo-modules-core"; import { useCallback, useEffect, useState } from "react"; import { Platform } from "react-native"; +import DeviceActivitySelectionSheetView from "./DeviceActivitySelectionSheetView"; +import DeviceActivitySelectionSheetViewPersisted from "./DeviceActivitySelectionSheetViewPersisted"; import DeviceActivitySelectionView from "./DeviceActivitySelectionView"; import DeviceActivitySelectionViewPersisted from "./DeviceActivitySelectionViewPersisted"; import { @@ -18,6 +20,8 @@ import { DeviceActivityEventRaw, DeviceActivityMonitorEventPayload, DeviceActivitySchedule, + DeviceActivitySelectionSheetViewPersistedProps, + DeviceActivitySelectionSheetViewProps, DeviceActivitySelectionViewPersistedProps, DeviceActivitySelectionViewProps, EventListenerMap, @@ -649,9 +653,16 @@ export function isAvailable(): boolean { ); } -export { DeviceActivitySelectionView, DeviceActivitySelectionViewPersisted }; +export { + DeviceActivitySelectionSheetView, + DeviceActivitySelectionSheetViewPersisted, + DeviceActivitySelectionView, + DeviceActivitySelectionViewPersisted, +}; export type { + DeviceActivitySelectionSheetViewProps as ReactNativeDeviceActivitySheetViewProps, + DeviceActivitySelectionSheetViewPersistedProps as ReactNativeDeviceActivitySheetViewPersistedProps, DeviceActivitySelectionViewProps as ReactNativeDeviceActivityViewProps, DeviceActivitySelectionViewPersistedProps as ReactNativeDeviceActivityViewPersistedProps, };