expo-app-blocker 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,6 @@ on:
7
7
  jobs:
8
8
  publish:
9
9
  runs-on: ubuntu-latest
10
- # Skip version bump commits to prevent loops
11
10
  if: "!startsWith(github.event.head_commit.message, 'v')"
12
11
 
13
12
  steps:
@@ -30,8 +29,16 @@ jobs:
30
29
  echo "bump=patch" >> $GITHUB_OUTPUT
31
30
  fi
32
31
 
33
- - name: Bump version (no git commit)
34
- run: npm version ${{ steps.version.outputs.bump }} --no-git-tag-version
32
+ - name: Sync version from npm and bump
33
+ run: |
34
+ # Get latest version from npm, fallback to package.json
35
+ NPM_VERSION=$(npm view expo-app-blocker version 2>/dev/null || echo "0.0.0")
36
+ echo "Current npm version: $NPM_VERSION"
37
+ # Set package.json to npm version first
38
+ npm version $NPM_VERSION --no-git-tag-version --allow-same-version
39
+ # Then bump
40
+ npm version ${{ steps.version.outputs.bump }} --no-git-tag-version
41
+ echo "New version: $(node -p 'require("./package.json").version')"
35
42
 
36
43
  - name: Publish to npm
37
44
  run: npm publish --access public
@@ -4,6 +4,6 @@
4
4
  "modules": ["expo.modules.appblocker.ExpoAppBlockerModule"]
5
5
  },
6
6
  "apple": {
7
- "modules": ["ExpoAppBlockerModule"]
7
+ "modules": ["ExpoAppBlockerModule", "ExpoAppBlockerPickerModule"]
8
8
  }
9
9
  }
@@ -0,0 +1,117 @@
1
+ import ExpoModulesCore
2
+ import FamilyControls
3
+ import ManagedSettings
4
+ import SwiftUI
5
+
6
+ public class ExpoAppBlockerPickerModule: Module {
7
+ public func definition() -> ModuleDefinition {
8
+ Name("ExpoAppBlockerPicker")
9
+
10
+ View(FamilyActivityPickerNativeView.self) {
11
+ Events("onSelectionChange")
12
+
13
+ Prop("initialSelection") { (view: FamilyActivityPickerNativeView, selectionBase64: String) in
14
+ guard !selectionBase64.isEmpty,
15
+ let data = Data(base64Encoded: selectionBase64),
16
+ let selection = try? JSONDecoder().decode(FamilyActivitySelection.self, from: data)
17
+ else { return }
18
+ view.setInitialSelection(selection)
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ // MARK: - ViewModel
25
+
26
+ class FamilyActivityPickerViewModel: ObservableObject {
27
+ @Published var selection = FamilyActivitySelection()
28
+ var didSetInitial = false
29
+ }
30
+
31
+ // MARK: - Native View (ExpoView wrapper)
32
+
33
+ class FamilyActivityPickerNativeView: ExpoView {
34
+ let onSelectionChange = EventDispatcher()
35
+ let viewModel = FamilyActivityPickerViewModel()
36
+ private var hostingController: UIHostingController<InlinePickerContentView>?
37
+
38
+ required init(appContext: AppContext? = nil) {
39
+ super.init(appContext: appContext)
40
+ clipsToBounds = true
41
+
42
+ let contentView = InlinePickerContentView(viewModel: viewModel) { [weak self] selection in
43
+ self?.handleSelectionChange(selection)
44
+ }
45
+ let hc = UIHostingController(rootView: contentView)
46
+ hc.view.backgroundColor = .clear
47
+ addSubview(hc.view)
48
+ hostingController = hc
49
+ }
50
+
51
+ override func layoutSubviews() {
52
+ super.layoutSubviews()
53
+ hostingController?.view.frame = bounds
54
+ }
55
+
56
+ func setInitialSelection(_ selection: FamilyActivitySelection) {
57
+ guard !viewModel.didSetInitial else { return }
58
+ viewModel.didSetInitial = true
59
+ viewModel.selection = selection
60
+ }
61
+
62
+ private func handleSelectionChange(_ selection: FamilyActivitySelection) {
63
+ var appItems: [[String: Any]] = []
64
+ for token in selection.applicationTokens {
65
+ if let data = try? JSONEncoder().encode(token) {
66
+ appItems.append([
67
+ "type": "app",
68
+ "token": data.base64EncodedString()
69
+ ])
70
+ }
71
+ }
72
+
73
+ var categoryItems: [[String: Any]] = []
74
+ for token in selection.categoryTokens {
75
+ if let data = try? JSONEncoder().encode(token) {
76
+ categoryItems.append([
77
+ "type": "category",
78
+ "token": data.base64EncodedString()
79
+ ])
80
+ }
81
+ }
82
+
83
+ var selectionBase64 = ""
84
+ if let selectionData = try? JSONEncoder().encode(selection) {
85
+ selectionBase64 = selectionData.base64EncodedString()
86
+ }
87
+
88
+ let items = appItems + categoryItems
89
+ let summary: [String: Any] = [
90
+ "type": "summary",
91
+ "totalApps": selection.applicationTokens.count,
92
+ "totalCategories": selection.categoryTokens.count,
93
+ "selectionData": selectionBase64
94
+ ]
95
+
96
+ onSelectionChange([
97
+ "items": items + [summary],
98
+ "totalApps": selection.applicationTokens.count,
99
+ "totalCategories": selection.categoryTokens.count,
100
+ "selectionData": selectionBase64
101
+ ])
102
+ }
103
+ }
104
+
105
+ // MARK: - SwiftUI Content View with inline FamilyActivityPicker
106
+
107
+ struct InlinePickerContentView: View {
108
+ @ObservedObject var viewModel: FamilyActivityPickerViewModel
109
+ var onSelectionChange: (FamilyActivitySelection) -> Void
110
+
111
+ var body: some View {
112
+ FamilyActivityPicker(selection: $viewModel.selection)
113
+ .onChange(of: viewModel.selection) { newSelection in
114
+ onSelectionChange(newSelection)
115
+ }
116
+ }
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Expo module for cross-platform app blocking. Android: UsageStatsManager + Overlay. iOS: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity).",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -61,6 +61,13 @@ export interface RelockResult {
61
61
  locked: boolean;
62
62
  }
63
63
 
64
+ export interface FamilyActivityPickerSelectionEvent {
65
+ items: IOSBlockedItem[];
66
+ totalApps: number;
67
+ totalCategories: number;
68
+ selectionData: string;
69
+ }
70
+
64
71
  // ──────────────────────────────────────────────────────────────────────────────
65
72
  // Plugin configuration types
66
73
  // ──────────────────────────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  IOSBlockConfiguration,
16
16
  TemporaryUnlockResult,
17
17
  RelockResult,
18
+ FamilyActivityPickerSelectionEvent,
18
19
  } from "./ExpoAppBlocker.types";
19
20
 
20
21
  export type {
@@ -28,6 +29,7 @@ export type {
28
29
  RelockResult,
29
30
  ShieldConfig,
30
31
  PluginConfig,
32
+ FamilyActivityPickerSelectionEvent,
31
33
  } from "./ExpoAppBlocker.types";
32
34
 
33
35
  // ──────────────────────────────────────────────────────────────────────────────
@@ -223,3 +225,36 @@ export function BlockedAppsNativeList({
223
225
  style: [{ minHeight: 50 }, style],
224
226
  });
225
227
  }
228
+
229
+ // ──────────────────────────────────────────────────────────────────────────────
230
+ // iOS Native View: inline FamilyActivityPicker (embedded in your UI)
231
+ // ──────────────────────────────────────────────────────────────────────────────
232
+
233
+ let NativePickerView: any = null;
234
+ if (Platform.OS === "ios") {
235
+ try {
236
+ NativePickerView = requireNativeViewManager("ExpoAppBlockerPicker");
237
+ } catch {}
238
+ }
239
+
240
+ export function FamilyActivityPickerView({
241
+ initialSelection,
242
+ onSelectionChange,
243
+ style,
244
+ }: {
245
+ /** Base64-encoded FamilyActivitySelection (from a previous picker result's selectionData) */
246
+ initialSelection?: string;
247
+ /** Called when the user changes selection in the picker */
248
+ onSelectionChange?: (event: FamilyActivityPickerSelectionEvent) => void;
249
+ style?: any;
250
+ }) {
251
+ if (!NativePickerView || Platform.OS !== "ios") return null;
252
+
253
+ return React.createElement(NativePickerView, {
254
+ initialSelection: initialSelection || "",
255
+ onSelectionChange: onSelectionChange
256
+ ? (e: any) => onSelectionChange(e.nativeEvent)
257
+ : undefined,
258
+ style: [{ minHeight: 400 }, style],
259
+ });
260
+ }
@@ -59,7 +59,7 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
59
59
  let hasSecondary = !shieldSecondaryButtonLabel.isEmpty && shieldSecondaryButtonLabel != "none"
60
60
 
61
61
  return ShieldConfiguration(
62
- backgroundBlurStyle: shieldBackgroundColor == nil ? .systemThickMaterial : nil,
62
+ backgroundBlurStyle: shieldBlurStyle,
63
63
  backgroundColor: shieldBackgroundColor,
64
64
  icon: mascotIcon,
65
65
  title: ShieldConfiguration.Label(text: shieldTitle, color: shieldTitleColor),