diff --git a/README.md b/README.md index 1c78b31..1f8f128 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Access the Screen Time API for iOS and Wellbeing API for Android (coming soon). - [Add FamilyControls capability to your app](#add-familycontrols-capability-to-your-app) - [Request Family Controls capabilities](#request-family-controls-capabilities) - [Set up for Expo](#set-up-for-expo) +- [App Store purchase restrictions](#app-store-purchase-restrictions) - [Sample code](#sample-code) - [Contributing](#contributing) - [Contributors](#contributors) @@ -110,6 +111,26 @@ Note: You'll need to install expo-build-properties if you haven't already: `expo install expo-build-properties` +## App Store purchase restrictions + +These methods mirror Apple's [`ManagedSettings.AppStoreSettings`](https://developer.apple.com/documentation/managedsettings/appstoresettings) `appStore` properties, and each takes a boolean matching the corresponding property: + +```typescript +// in-app purchases (AppStoreSettings.denyInAppPurchases) +await ScreenTime.denyInAppPurchases(true); // block in-app purchases +await ScreenTime.denyInAppPurchases(false); // allow in-app purchases + +// require a password for purchases (AppStoreSettings.requirePasswordForPurchases) +await ScreenTime.requirePasswordForPurchases(true); +await ScreenTime.requirePasswordForPurchases(false); +``` + +On iOS the current values can be read back from [`getStore()`](#sample-code) under `appStore`. + +> **`allowInAppPurchases()` is deprecated.** Apple's `AppStoreSettings` has no `allowInAppPurchases` property, so use `denyInAppPurchases(false)` instead. `allowInAppPurchases()` is kept only for backwards compatibility. + +> **⚠️ Not supported on Android.** Android exposes no system-level API to block in-app purchases or require a password for purchases for a regular app. These methods exist on Android only to mirror the API surface — they persist the flag state but do **not** enforce any restriction. + ## Sample code ```typescript import React from 'react'; diff --git a/android/src/main/java/com/noodleofdeath/screentimeapi/ScreenTimeAPIModule.java b/android/src/main/java/com/noodleofdeath/screentimeapi/ScreenTimeAPIModule.java index abfb413..da858ee 100644 --- a/android/src/main/java/com/noodleofdeath/screentimeapi/ScreenTimeAPIModule.java +++ b/android/src/main/java/com/noodleofdeath/screentimeapi/ScreenTimeAPIModule.java @@ -1,6 +1,8 @@ package com.noodleofdeath.screentimeapi; import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import androidx.annotation.NonNull; @@ -16,10 +18,19 @@ public class ScreenTimeAPIModule extends ReactContextBaseJavaModule { private static final int REQUEST_CODE_ENABLE_ADMIN = 1; + private static final String PREFS_NAME = "ScreenTimeAPIPrefs"; + private static final String KEY_DENY_IN_APP_PURCHASES = "denyInAppPurchases"; + private static final String KEY_REQUIRE_PASSWORD_FOR_PURCHASES = "requirePasswordForPurchases"; + public ScreenTimeAPIModule(ReactApplicationContext reactContext) { super(reactContext); } + private SharedPreferences getPrefs() { + return getReactApplicationContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + @NonNull @Override public String getName() { @@ -57,23 +68,75 @@ public void clearBlockedApplications(Promise promise) { } @ReactMethod - public void denyAppRemoval(Promise promise) { + public void denyAppRemoval(boolean deny, Promise promise) { promise.resolve("success"); } + /** + * @deprecated Apple's {@code ApplicationSettings} has no {@code allowAppRemoval} + * property; use {@link #denyAppRemoval(boolean, Promise) denyAppRemoval(false)} + * instead. Kept for backwards compatibility. + */ + @Deprecated @ReactMethod public void allowAppRemoval(Promise promise) { promise.resolve("success"); } @ReactMethod - public void denyAppInstallation(Promise promise) { + public void denyAppInstallation(boolean deny, Promise promise) { promise.resolve("success"); } + /** + * @deprecated Apple's {@code ApplicationSettings} has no {@code allowAppInstallation} + * property; use {@link #denyAppInstallation(boolean, Promise) denyAppInstallation(false)} + * instead. Kept for backwards compatibility. + */ + @Deprecated @ReactMethod public void allowAppInstallation(Promise promise) { promise.resolve("success"); } + // App Store purchase restrictions are not supported on Android: the OS + // exposes no system-level API to block in-app purchases or require a + // password for purchases for a regular app. The flags are only persisted + // here to mirror the iOS API; they are NOT enforced by the OS. + + /** + * Not supported on Android. In-app purchase blocking has no Android + * equivalent; the flag is persisted but never enforced by the OS. + */ + @ReactMethod + public void denyInAppPurchases(boolean deny, Promise promise) { + getPrefs().edit().putBoolean(KEY_DENY_IN_APP_PURCHASES, deny).apply(); + promise.resolve("success"); + } + + /** + * Not supported on Android. In-app purchase blocking has no Android + * equivalent; the flag is persisted but never enforced by the OS. + * + * @deprecated Apple's {@code AppStoreSettings} has no {@code allowInAppPurchases} + * property; use {@link #denyInAppPurchases(boolean, Promise) denyInAppPurchases(false)} + * instead. Kept for backwards compatibility. + */ + @Deprecated + @ReactMethod + public void allowInAppPurchases(Promise promise) { + getPrefs().edit().putBoolean(KEY_DENY_IN_APP_PURCHASES, false).apply(); + promise.resolve("success"); + } + + /** + * Not supported on Android. Requiring a password for purchases has no + * Android equivalent; the flag is persisted but never enforced by the OS. + */ + @ReactMethod + public void requirePasswordForPurchases(boolean req, Promise promise) { + getPrefs().edit().putBoolean(KEY_REQUIRE_PASSWORD_FOR_PURCHASES, req).apply(); + promise.resolve("success"); + } + } \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 04da149..677f07b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -277,13 +277,25 @@ export type IScreenTimeAPI = { clearBlockedApplications: () => Promise; /** + * Sets whether app installation is denied. + * + * Mirrors Apple's + * [`ApplicationSettings.denyAppInstallation`](https://developer.apple.com/documentation/managedsettings/applicationsettings/denyappinstallation). + * On iOS this sets `ManagedSettingsStore.application.denyAppInstallation`; the + * current value is readable via {@link getStore} (`application.denyAppInstallation`). * @platform ios * @platform android + * @param {boolean} deny whether app installation is denied * @returns {Promise} */ - denyAppInstallation: () => Promise; + denyAppInstallation: (deny: boolean) => Promise; /** + * Allows app installation. + * + * @deprecated Apple's `ApplicationSettings` has no `allowAppInstallation` + * property; use {@link denyAppInstallation}`(false)` instead. Kept for + * backwards compatibility. * @platform ios * @platform android * @returns {Promise} @@ -291,19 +303,77 @@ export type IScreenTimeAPI = { allowAppInstallation: () => Promise; /** + * Sets whether app removal is denied. + * + * Mirrors Apple's + * [`ApplicationSettings.denyAppRemoval`](https://developer.apple.com/documentation/managedsettings/applicationsettings/denyappremoval). + * On iOS this sets `ManagedSettingsStore.application.denyAppRemoval`; the + * current value is readable via {@link getStore} (`application.denyAppRemoval`). * @platform ios * @platform android + * @param {boolean} deny whether app removal is denied * @returns {Promise} */ - denyAppRemoval: () => Promise; + denyAppRemoval: (deny: boolean) => Promise; /** + * Allows app removal. + * + * @deprecated Apple's `ApplicationSettings` has no `allowAppRemoval` + * property; use {@link denyAppRemoval}`(false)` instead. Kept for + * backwards compatibility. * @platform ios * @platform android * @returns {Promise} */ allowAppRemoval: () => Promise; - + + /** + * Sets whether in-app purchases are denied. + * + * Mirrors Apple's + * [`AppStoreSettings.denyInAppPurchases`](https://developer.apple.com/documentation/managedsettings/appstoresettings/denyinapppurchases). + * On iOS this sets `ManagedSettingsStore.appStore.denyInAppPurchases`; the + * current value is readable via {@link getStore} (`appStore.denyInAppPurchases`). + * + * Not supported on Android — Android exposes no system-level API to block + * in-app purchases for a regular app, so this call has no effect there. + * @platform ios + * @param {boolean} deny whether in-app purchases are denied + * @returns {Promise} + */ + denyInAppPurchases: (deny: boolean) => Promise; + + /** + * Allows in-app purchases. + * + * @deprecated Apple's `AppStoreSettings` has no `allowInAppPurchases` + * property; use {@link denyInAppPurchases}`(false)` instead. Kept for + * backwards compatibility. + * + * Not supported on Android — Android exposes no system-level API to block + * in-app purchases for a regular app, so this call has no effect there. + * @platform ios + * @returns {Promise} + */ + allowInAppPurchases: () => Promise; + + /** + * Sets whether a password is required for purchases. + * + * Mirrors Apple's + * [`AppStoreSettings.requirePasswordForPurchases`](https://developer.apple.com/documentation/managedsettings/appstoresettings/requirepasswordforpurchases). + * On iOS this sets `ManagedSettingsStore.appStore.requirePasswordForPurchases`; + * the current value is readable via {@link getStore} (`appStore.requirePasswordForPurchases`). + * + * Not supported on Android — Android exposes no system-level API to require + * a password for purchases for a regular app, so this call has no effect there. + * @platform ios + * @param {boolean} req whether a password is required for purchases + * @returns {Promise} + */ + requirePasswordForPurchases: (req: boolean) => Promise; + /** * @platform ios * @returns {Promise} diff --git a/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.m b/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.m index 1b29258..2a8abb2 100644 --- a/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.m +++ b/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.m @@ -28,10 +28,13 @@ @interface RCT_EXTERN_MODULE(ScreenTimeAPI, NSObject) resolver: (RCTPromiseResolveBlock) resolve rejecter: (RCTPromiseRejectBlock) reject) RCT_EXTERN_METHOD(clearActivitySelection) -RCT_EXTERN_METHOD(denyAppRemoval) +RCT_EXTERN_METHOD(denyAppRemoval: (BOOL) deny) RCT_EXTERN_METHOD(allowAppRemoval) -RCT_EXTERN_METHOD(denyAppInstallation) +RCT_EXTERN_METHOD(denyAppInstallation: (BOOL) deny) RCT_EXTERN_METHOD(allowAppInstallation) +RCT_EXTERN_METHOD(denyInAppPurchases: (BOOL) deny) +RCT_EXTERN_METHOD(allowInAppPurchases) +RCT_EXTERN_METHOD(requirePasswordForPurchases: (BOOL) req) RCT_EXTERN_METHOD(displayFamilyActivityPicker: (NSDictionary *) options resolver: (RCTPromiseResolveBlock) resolve rejecter: (RCTPromiseRejectBlock) reject) diff --git a/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.swift b/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.swift index dda6a2f..fabb510 100644 --- a/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.swift +++ b/ios/ReactNativeScreenTimeAPI/ReactNativeScreenTimeAPI.swift @@ -212,25 +212,46 @@ public class ScreenTimeAPI: NSObject { } @objc - public func denyAppRemoval() { - store.application.denyAppRemoval = true + public func denyAppRemoval(_ deny: Bool) { + store.application.denyAppRemoval = deny } - + + /// Deprecated: `ApplicationSettings` has no `allowAppRemoval` property. + /// Use `denyAppRemoval(false)` instead. Kept for backwards compatibility. @objc public func allowAppRemoval() { store.application.denyAppRemoval = false } - + @objc - public func denyAppInstallation() { - store.application.denyAppInstallation = true + public func denyAppInstallation(_ deny: Bool) { + store.application.denyAppInstallation = deny } - + + /// Deprecated: `ApplicationSettings` has no `allowAppInstallation` property. + /// Use `denyAppInstallation(false)` instead. Kept for backwards compatibility. @objc public func allowAppInstallation() { store.application.denyAppInstallation = false } - + + @objc + public func denyInAppPurchases(_ deny: Bool) { + store.appStore.denyInAppPurchases = deny + } + + /// Deprecated: `AppStoreSettings` has no `allowInAppPurchases` property. + /// Use `denyInAppPurchases(false)` instead. Kept for backwards compatibility. + @objc + public func allowInAppPurchases() { + store.appStore.denyInAppPurchases = false + } + + @objc + public func requirePasswordForPurchases(_ req: Bool) { + store.appStore.requirePasswordForPurchases = req + } + @objc public func initializeMonitoring(_ startTimestamp: String = "00:00", end endTimestamp: String = "23:59",