expo-app-blocker 0.1.1 → 0.1.3
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.
- package/.github/workflows/publish.yml +42 -0
- package/README.md +12 -5
- package/docs/APPLE_DEVELOPER_SETUP.md +161 -0
- package/package.json +1 -1
- package/plugin/src/index.js +25 -5
- package/src/ExpoAppBlocker.types.ts +27 -9
- package/targets/ShieldConfiguration/ShieldConfigurationExtension.swift +99 -105
- package/ios/ExpoAppBlocker.podspec +0 -21
- package/ios/ExpoAppBlockerConfig.swift +0 -19
- package/ios/ExpoAppBlockerModule.swift +0 -1014
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
# Skip version bump commits to prevent loops
|
|
11
|
+
if: "!startsWith(github.event.head_commit.message, 'v')"
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
registry-url: https://registry.npmjs.org
|
|
20
|
+
|
|
21
|
+
- name: Determine version bump
|
|
22
|
+
id: version
|
|
23
|
+
run: |
|
|
24
|
+
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
25
|
+
if echo "$COMMIT_MSG" | grep -qw "\[major\]"; then
|
|
26
|
+
echo "bump=major" >> $GITHUB_OUTPUT
|
|
27
|
+
elif echo "$COMMIT_MSG" | grep -qw "\[minor\]"; then
|
|
28
|
+
echo "bump=minor" >> $GITHUB_OUTPUT
|
|
29
|
+
else
|
|
30
|
+
echo "bump=patch" >> $GITHUB_OUTPUT
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- name: Bump version (no git commit)
|
|
34
|
+
run: npm version ${{ steps.version.outputs.bump }} --no-git-tag-version
|
|
35
|
+
|
|
36
|
+
- name: Publish to npm
|
|
37
|
+
run: npm publish --access public
|
|
38
|
+
env:
|
|
39
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
40
|
+
|
|
41
|
+
- name: Output version
|
|
42
|
+
run: echo "Published $(node -p 'require("./package.json").version')"
|
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Cross-platform app blocking module for Expo. Block other apps and redirect users
|
|
|
22
22
|
|
|
23
23
|
### Apple Developer Portal (iOS)
|
|
24
24
|
|
|
25
|
+
> **Full step-by-step guide**: [docs/APPLE_DEVELOPER_SETUP.md](docs/APPLE_DEVELOPER_SETUP.md)
|
|
26
|
+
|
|
25
27
|
1. Register **4 App IDs** with **Family Controls** and **App Groups** capabilities:
|
|
26
28
|
- `com.yourapp.id` (main app)
|
|
27
29
|
- `com.yourapp.id.DeviceActivityMonitor`
|
|
@@ -85,13 +87,18 @@ Add the plugin to your `app.json`:
|
|
|
85
87
|
| Option | Type | Default | Description |
|
|
86
88
|
|---|---|---|---|
|
|
87
89
|
| `ios.appGroup` | `string` | Required | App Group identifier for shared data |
|
|
88
|
-
| `ios.shield.title` | `string` | `"Hold on!"` | Shield
|
|
89
|
-
| `ios.shield.
|
|
90
|
+
| `ios.shield.title` | `string` | `"Hold on!"` | Shield title. `{appName}` replaced with blocked app name |
|
|
91
|
+
| `ios.shield.titleColor` | `string` | `"#111111"` | Title text color (hex) |
|
|
92
|
+
| `ios.shield.subtitle` | `string` | `"{appName} is blocked."` | Shield subtitle. `{appName}` replaced with blocked app name |
|
|
93
|
+
| `ios.shield.subtitleColor` | `string` | `"#8c8c8c"` | Subtitle text color (hex) |
|
|
90
94
|
| `ios.shield.primaryButtonLabel` | `string` | `"Earn Free Time"` | Primary button text |
|
|
95
|
+
| `ios.shield.primaryButtonLabelColor` | `string` | `"#ffffff"` | Primary button text color (hex) |
|
|
96
|
+
| `ios.shield.primaryButtonBackgroundColor` | `string` | `"#7cb518"` | Primary button background color (hex) |
|
|
91
97
|
| `ios.shield.secondaryButtonLabel` | `string\|null` | `"Not now"` | Secondary button text. Set to `null` to hide |
|
|
92
|
-
| `ios.shield.
|
|
93
|
-
| `ios.shield.
|
|
94
|
-
| `ios.shield.
|
|
98
|
+
| `ios.shield.secondaryButtonLabelColor` | `string` | `"#8c8c8c"` | Secondary button text color (hex) |
|
|
99
|
+
| `ios.shield.backgroundColor` | `string\|null` | `null` | Background tint color over blur (hex, supports alpha e.g. `"#FF000033"`) |
|
|
100
|
+
| `ios.shield.backgroundBlurStyle` | `string` | `"systemThickMaterial"` | Blur style. Options: `extraLight`, `light`, `dark`, `regular`, `prominent`, `systemUltraThinMaterial`, `systemThinMaterial`, `systemMaterial`, `systemThickMaterial`, `systemChromeMaterial`, and light/dark forced variants |
|
|
101
|
+
| `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (relative to project root) |
|
|
95
102
|
| `android.notificationTitle` | `string` | `"App Blocked"` | Notification title |
|
|
96
103
|
| `android.notificationText` | `string` | `"{appName} is blocked."` | Notification text |
|
|
97
104
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Apple Developer Portal Setup
|
|
2
|
+
|
|
3
|
+
This guide walks you through the one-time setup required in the Apple Developer Portal for `expo-app-blocker` on iOS.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- A **paid Apple Developer account** ($99/year)
|
|
8
|
+
- Access to https://developer.apple.com/account
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1: Create the App Group
|
|
13
|
+
|
|
14
|
+
The App Group enables data sharing between your main app and the three iOS extensions.
|
|
15
|
+
|
|
16
|
+
1. Go to **Identifiers**: https://developer.apple.com/account/resources/identifiers/list
|
|
17
|
+
2. Change the dropdown from **"App IDs"** to **"App Groups"**
|
|
18
|
+
- Or go directly to: https://developer.apple.com/account/resources/identifiers/list/applicationGroup
|
|
19
|
+
3. Click the **+** (blue plus) button
|
|
20
|
+
4. Select **App Groups** > **Continue**
|
|
21
|
+
5. Fill in:
|
|
22
|
+
- **Description**: `Your App Name Shared` (e.g., "My App Blocker Shared")
|
|
23
|
+
- **Identifier**: The value you set in `ios.appGroup` in your plugin config
|
|
24
|
+
- Example: `group.com.yourapp.blocker`
|
|
25
|
+
6. Click **Continue** > **Register**
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Step 2: Register the Main App ID
|
|
30
|
+
|
|
31
|
+
1. Go to **Identifiers**: https://developer.apple.com/account/resources/identifiers/list
|
|
32
|
+
2. Make sure the dropdown shows **"App IDs"**
|
|
33
|
+
3. Click the **+** button
|
|
34
|
+
4. Select **App IDs** > **Continue**
|
|
35
|
+
5. Select **App** > **Continue**
|
|
36
|
+
6. Fill in:
|
|
37
|
+
- **Description**: Your app name (e.g., "My App Blocker")
|
|
38
|
+
- **Bundle ID**: Select **Explicit**, enter your `ios.bundleIdentifier` from `app.json`
|
|
39
|
+
- Example: `com.yourapp.id`
|
|
40
|
+
7. Scroll down to **Capabilities** and enable:
|
|
41
|
+
- **App Groups**
|
|
42
|
+
- **Family Controls**
|
|
43
|
+
8. Click **Continue** > **Register**
|
|
44
|
+
|
|
45
|
+
> **Note on Family Controls**: If you don't see Family Controls in the capabilities list, you may need to request it. Look for a link to request additional capabilities, or check if it appears under "Additional Capabilities".
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Step 3: Register the DeviceActivityMonitor Extension App ID
|
|
50
|
+
|
|
51
|
+
1. Click the **+** button again
|
|
52
|
+
2. **App IDs** > **App** > **Continue**
|
|
53
|
+
3. Fill in:
|
|
54
|
+
- **Description**: `Your App DeviceActivityMonitor`
|
|
55
|
+
- **Bundle ID**: Explicit, enter `{your-bundle-id}.DeviceActivityMonitor`
|
|
56
|
+
- Example: `com.yourapp.id.DeviceActivityMonitor`
|
|
57
|
+
4. Enable capabilities:
|
|
58
|
+
- **App Groups**
|
|
59
|
+
- **Family Controls**
|
|
60
|
+
5. Click **Continue** > **Register**
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Step 4: Register the ShieldAction Extension App ID
|
|
65
|
+
|
|
66
|
+
1. Click the **+** button
|
|
67
|
+
2. **App IDs** > **App** > **Continue**
|
|
68
|
+
3. Fill in:
|
|
69
|
+
- **Description**: `Your App ShieldAction`
|
|
70
|
+
- **Bundle ID**: Explicit, enter `{your-bundle-id}.ShieldAction`
|
|
71
|
+
- Example: `com.yourapp.id.ShieldAction`
|
|
72
|
+
4. Enable capabilities:
|
|
73
|
+
- **App Groups**
|
|
74
|
+
- **Family Controls**
|
|
75
|
+
5. Click **Continue** > **Register**
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 5: Register the ShieldConfiguration Extension App ID
|
|
80
|
+
|
|
81
|
+
1. Click the **+** button
|
|
82
|
+
2. **App IDs** > **App** > **Continue**
|
|
83
|
+
3. Fill in:
|
|
84
|
+
- **Description**: `Your App ShieldConfiguration`
|
|
85
|
+
- **Bundle ID**: Explicit, enter `{your-bundle-id}.ShieldConfiguration`
|
|
86
|
+
- Example: `com.yourapp.id.ShieldConfiguration`
|
|
87
|
+
4. Enable capabilities:
|
|
88
|
+
- **App Groups**
|
|
89
|
+
- **Family Controls**
|
|
90
|
+
5. Click **Continue** > **Register**
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Step 6: Assign the App Group to All App IDs
|
|
95
|
+
|
|
96
|
+
For **each of the 4 App IDs** you just created:
|
|
97
|
+
|
|
98
|
+
1. Click on the App ID in the list
|
|
99
|
+
2. Scroll to **App Groups**
|
|
100
|
+
3. Click **Configure** (or **Edit**)
|
|
101
|
+
4. Check your App Group (e.g., `group.com.yourapp.blocker`)
|
|
102
|
+
5. Click **Save**
|
|
103
|
+
|
|
104
|
+
Repeat for all 4:
|
|
105
|
+
- `com.yourapp.id`
|
|
106
|
+
- `com.yourapp.id.DeviceActivityMonitor`
|
|
107
|
+
- `com.yourapp.id.ShieldAction`
|
|
108
|
+
- `com.yourapp.id.ShieldConfiguration`
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Summary Checklist
|
|
113
|
+
|
|
114
|
+
When you're done, you should have:
|
|
115
|
+
|
|
116
|
+
- [ ] **1 App Group**: `group.com.yourapp.blocker`
|
|
117
|
+
- [ ] **4 App IDs**, each with Family Controls + App Groups:
|
|
118
|
+
|
|
119
|
+
| App ID | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `com.yourapp.id` | Main app |
|
|
122
|
+
| `com.yourapp.id.DeviceActivityMonitor` | Relock timer extension |
|
|
123
|
+
| `com.yourapp.id.ShieldAction` | Shield button handler extension |
|
|
124
|
+
| `com.yourapp.id.ShieldConfiguration` | Custom shield UI extension |
|
|
125
|
+
|
|
126
|
+
- [ ] App Group assigned to all 4 App IDs
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## About Family Controls Approval
|
|
131
|
+
|
|
132
|
+
- **Development builds** (run from Xcode): Family Controls works **without** formal Apple approval
|
|
133
|
+
- **TestFlight**: May require approval depending on your account
|
|
134
|
+
- **App Store**: Requires Family Controls capability approval from Apple
|
|
135
|
+
|
|
136
|
+
To request approval:
|
|
137
|
+
1. Go to https://developer.apple.com/contact/request/family-controls-distribution
|
|
138
|
+
2. Fill out the form explaining your app's use case
|
|
139
|
+
3. Wait for Apple's response (can take days to weeks)
|
|
140
|
+
|
|
141
|
+
**You can develop and test locally without waiting for approval.**
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Troubleshooting
|
|
146
|
+
|
|
147
|
+
**"Family Controls" not visible in capabilities list**
|
|
148
|
+
- Make sure you're on a paid developer account (not free)
|
|
149
|
+
- Try searching for it in the capabilities search bar
|
|
150
|
+
- You may need to request access: https://developer.apple.com/contact/request/family-controls-distribution
|
|
151
|
+
|
|
152
|
+
**"An App ID with this identifier is not available"**
|
|
153
|
+
- The bundle ID might already be registered. Check your existing identifiers.
|
|
154
|
+
|
|
155
|
+
**App Group not showing when configuring an App ID**
|
|
156
|
+
- Make sure you created the App Group first (Step 1)
|
|
157
|
+
- Try refreshing the page
|
|
158
|
+
|
|
159
|
+
**Signing errors in Xcode after setup**
|
|
160
|
+
- In Xcode: select each target > Signing & Capabilities > set your Team
|
|
161
|
+
- Xcode should automatically create provisioning profiles using the registered App IDs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-app-blocker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|
package/plugin/src/index.js
CHANGED
|
@@ -141,7 +141,23 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// Inject
|
|
144
|
+
// Inject config values into extension Swift files
|
|
145
|
+
const shield = pluginConfig?.ios?.shield || {};
|
|
146
|
+
const replacements = {
|
|
147
|
+
"APP_GROUP_PLACEHOLDER": appGroup,
|
|
148
|
+
"SHIELD_TITLE_PLACEHOLDER": shield.title || "Hold on!",
|
|
149
|
+
"SHIELD_TITLE_COLOR_PLACEHOLDER": shield.titleColor || "#111111",
|
|
150
|
+
"SHIELD_SUBTITLE_PLACEHOLDER": shield.subtitle || "{appName} is blocked.",
|
|
151
|
+
"SHIELD_SUBTITLE_COLOR_PLACEHOLDER": shield.subtitleColor || "#8c8c8c",
|
|
152
|
+
"SHIELD_PRIMARY_LABEL_PLACEHOLDER": shield.primaryButtonLabel || "Earn Free Time",
|
|
153
|
+
"SHIELD_PRIMARY_LABEL_COLOR_PLACEHOLDER": shield.primaryButtonLabelColor || "#ffffff",
|
|
154
|
+
"SHIELD_PRIMARY_BG_COLOR_PLACEHOLDER": shield.primaryButtonBackgroundColor || shield.primaryButtonColor || "#7cb518",
|
|
155
|
+
"SHIELD_SECONDARY_LABEL_PLACEHOLDER": shield.secondaryButtonLabel === null ? "NONE" : (shield.secondaryButtonLabel || "Not now"),
|
|
156
|
+
"SHIELD_SECONDARY_LABEL_COLOR_PLACEHOLDER": shield.secondaryButtonLabelColor || "#8c8c8c",
|
|
157
|
+
"SHIELD_BG_COLOR_PLACEHOLDER": shield.backgroundColor || "NONE",
|
|
158
|
+
"SHIELD_BLUR_STYLE_PLACEHOLDER": shield.backgroundBlurStyle || "systemThickMaterial",
|
|
159
|
+
};
|
|
160
|
+
|
|
145
161
|
const targetsDir = path.join(path.dirname(platformRoot), "targets");
|
|
146
162
|
if (fs.existsSync(targetsDir)) {
|
|
147
163
|
const dirs = fs.readdirSync(targetsDir);
|
|
@@ -153,16 +169,20 @@ function withAppBlockerIOS(config, pluginConfig) {
|
|
|
153
169
|
if (!file.endsWith(".swift")) continue;
|
|
154
170
|
const filePath = path.join(dirPath, file);
|
|
155
171
|
let content = fs.readFileSync(filePath, "utf-8");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
let changed = false;
|
|
173
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
174
|
+
if (content.includes(placeholder)) {
|
|
175
|
+
content = content.replace(new RegExp(placeholder, "g"), value);
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
159
178
|
}
|
|
179
|
+
if (changed) fs.writeFileSync(filePath, content);
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
// Copy shield icon to ShieldConfiguration target assets
|
|
165
|
-
const shieldIcon =
|
|
185
|
+
const shieldIcon = shield.icon;
|
|
166
186
|
if (shieldIcon) {
|
|
167
187
|
const projectRoot = path.dirname(platformRoot);
|
|
168
188
|
const iconSrc = path.resolve(projectRoot, shieldIcon);
|
|
@@ -66,21 +66,39 @@ export interface RelockResult {
|
|
|
66
66
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
68
|
export interface ShieldConfig {
|
|
69
|
-
/** Title
|
|
69
|
+
/** Title text. Use {appName} as placeholder for the blocked app name. Default: "Hold on!" */
|
|
70
70
|
title?: string;
|
|
71
|
-
/**
|
|
71
|
+
/** Title text color (hex). Default: "#111111" */
|
|
72
|
+
titleColor?: string;
|
|
73
|
+
/** Subtitle text. Use {appName} as placeholder. Default: "{appName} is blocked." */
|
|
72
74
|
subtitle?: string;
|
|
73
|
-
/**
|
|
75
|
+
/** Subtitle text color (hex). Default: "#8c8c8c" */
|
|
76
|
+
subtitleColor?: string;
|
|
77
|
+
/** Primary button label text. Default: "Earn Free Time" */
|
|
74
78
|
primaryButtonLabel?: string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
79
|
+
/** Primary button label text color (hex). Default: "#ffffff" */
|
|
80
|
+
primaryButtonLabelColor?: string;
|
|
77
81
|
/** Primary button background color (hex). Default: "#7cb518" */
|
|
78
|
-
|
|
79
|
-
/**
|
|
82
|
+
primaryButtonBackgroundColor?: string;
|
|
83
|
+
/** Secondary button label text. Set to null to hide the button. Default: "Not now" */
|
|
84
|
+
secondaryButtonLabel?: string | null;
|
|
85
|
+
/** Secondary button label text color (hex). Default: "#8c8c8c" */
|
|
86
|
+
secondaryButtonLabelColor?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Background tint color (hex, supports alpha e.g. "#FF000033").
|
|
89
|
+
* Applied as overlay on top of the blur. Default: null (no tint)
|
|
90
|
+
*/
|
|
80
91
|
backgroundColor?: string | null;
|
|
81
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Background blur style. Default: "systemThickMaterial"
|
|
94
|
+
*
|
|
95
|
+
* Options: "extraLight", "light", "dark", "regular", "prominent",
|
|
96
|
+
* "systemUltraThinMaterial", "systemThinMaterial", "systemMaterial",
|
|
97
|
+
* "systemThickMaterial", "systemChromeMaterial",
|
|
98
|
+
* and light/dark forced variants (e.g. "systemMaterialDark")
|
|
99
|
+
*/
|
|
82
100
|
backgroundBlurStyle?: string;
|
|
83
|
-
/** Path to shield icon
|
|
101
|
+
/** Path to custom shield icon PNG (relative to project root). Optional. */
|
|
84
102
|
icon?: string;
|
|
85
103
|
}
|
|
86
104
|
|
|
@@ -6,15 +6,71 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
6
6
|
|
|
7
7
|
private let appGroupIdentifier = "APP_GROUP_PLACEHOLDER"
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
private let
|
|
11
|
-
private let
|
|
12
|
-
private let
|
|
13
|
-
private let
|
|
14
|
-
private let
|
|
9
|
+
// ── Configurable values (injected by plugin at prebuild) ──────────────
|
|
10
|
+
private let cfgTitle = "SHIELD_TITLE_PLACEHOLDER" // default: "Hold on!"
|
|
11
|
+
private let cfgTitleColor = "SHIELD_TITLE_COLOR_PLACEHOLDER" // default: "#111111"
|
|
12
|
+
private let cfgSubtitle = "SHIELD_SUBTITLE_PLACEHOLDER" // default: "{appName} is blocked."
|
|
13
|
+
private let cfgSubtitleColor = "SHIELD_SUBTITLE_COLOR_PLACEHOLDER" // default: "#8c8c8c"
|
|
14
|
+
private let cfgPrimaryLabel = "SHIELD_PRIMARY_LABEL_PLACEHOLDER" // default: "Earn Free Time"
|
|
15
|
+
private let cfgPrimaryLabelColor = "SHIELD_PRIMARY_LABEL_COLOR_PLACEHOLDER" // default: "#ffffff"
|
|
16
|
+
private let cfgPrimaryBgColor = "SHIELD_PRIMARY_BG_COLOR_PLACEHOLDER" // default: "#7cb518"
|
|
17
|
+
private let cfgSecondaryLabel = "SHIELD_SECONDARY_LABEL_PLACEHOLDER" // default: "Not now", "NONE" to hide
|
|
18
|
+
private let cfgSecondaryLabelColor = "SHIELD_SECONDARY_LABEL_COLOR_PLACEHOLDER" // default: "#8c8c8c"
|
|
19
|
+
private let cfgBgColor = "SHIELD_BG_COLOR_PLACEHOLDER" // default: "NONE" (no tint)
|
|
20
|
+
private let cfgBlurStyle = "SHIELD_BLUR_STYLE_PLACEHOLDER" // default: "systemThickMaterial"
|
|
21
|
+
|
|
22
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
private func hexColor(_ hex: String) -> UIColor {
|
|
25
|
+
var h = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
26
|
+
if h.hasPrefix("#") { h.removeFirst() }
|
|
27
|
+
|
|
28
|
+
var rgb: UInt64 = 0
|
|
29
|
+
Scanner(string: h).scanHexInt64(&rgb)
|
|
30
|
+
|
|
31
|
+
if h.count == 8 { // RRGGBBAA
|
|
32
|
+
return UIColor(
|
|
33
|
+
red: CGFloat((rgb >> 24) & 0xFF) / 255,
|
|
34
|
+
green: CGFloat((rgb >> 16) & 0xFF) / 255,
|
|
35
|
+
blue: CGFloat((rgb >> 8) & 0xFF) / 255,
|
|
36
|
+
alpha: CGFloat(rgb & 0xFF) / 255
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return UIColor(
|
|
40
|
+
red: CGFloat((rgb >> 16) & 0xFF) / 255,
|
|
41
|
+
green: CGFloat((rgb >> 8) & 0xFF) / 255,
|
|
42
|
+
blue: CGFloat(rgb & 0xFF) / 255,
|
|
43
|
+
alpha: 1.0
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func resolveBlurStyle(_ name: String) -> UIBlurEffect.Style {
|
|
48
|
+
switch name {
|
|
49
|
+
case "extraLight": return .extraLight
|
|
50
|
+
case "light": return .light
|
|
51
|
+
case "dark": return .dark
|
|
52
|
+
case "regular": return .regular
|
|
53
|
+
case "prominent": return .prominent
|
|
54
|
+
case "systemUltraThinMaterial": return .systemUltraThinMaterial
|
|
55
|
+
case "systemThinMaterial": return .systemThinMaterial
|
|
56
|
+
case "systemMaterial": return .systemMaterial
|
|
57
|
+
case "systemThickMaterial": return .systemThickMaterial
|
|
58
|
+
case "systemChromeMaterial": return .systemChromeMaterial
|
|
59
|
+
case "systemUltraThinMaterialLight": return .systemUltraThinMaterialLight
|
|
60
|
+
case "systemThinMaterialLight": return .systemThinMaterialLight
|
|
61
|
+
case "systemMaterialLight": return .systemMaterialLight
|
|
62
|
+
case "systemThickMaterialLight": return .systemThickMaterialLight
|
|
63
|
+
case "systemChromeMaterialLight": return .systemChromeMaterialLight
|
|
64
|
+
case "systemUltraThinMaterialDark": return .systemUltraThinMaterialDark
|
|
65
|
+
case "systemThinMaterialDark": return .systemThinMaterialDark
|
|
66
|
+
case "systemMaterialDark": return .systemMaterialDark
|
|
67
|
+
case "systemThickMaterialDark": return .systemThickMaterialDark
|
|
68
|
+
case "systemChromeMaterialDark": return .systemChromeMaterialDark
|
|
69
|
+
default: return .systemThickMaterial
|
|
70
|
+
}
|
|
71
|
+
}
|
|
15
72
|
|
|
16
73
|
private var mascotIcon: UIImage? {
|
|
17
|
-
// Load from the extension's own bundle
|
|
18
74
|
let bundle = Bundle(for: type(of: self))
|
|
19
75
|
return UIImage(named: "shield-icon", in: bundle, compatibleWith: nil)
|
|
20
76
|
?? UIImage(contentsOfFile: bundle.path(forResource: "shield-icon", ofType: "png") ?? "")
|
|
@@ -23,130 +79,68 @@ class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
|
23
79
|
private func getBlockedAppCount() -> Int {
|
|
24
80
|
guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return 0 }
|
|
25
81
|
guard let config = defaults.dictionary(forKey: "appBlocker.blockConfiguration.v1") else { return 0 }
|
|
26
|
-
if let items = config["blockedItems"] as? [[String: Any]] {
|
|
27
|
-
return items.count
|
|
28
|
-
}
|
|
82
|
+
if let items = config["blockedItems"] as? [[String: Any]] { return items.count }
|
|
29
83
|
return 0
|
|
30
84
|
}
|
|
31
85
|
|
|
32
86
|
private func isTemporarilyUnlocked() -> Bool {
|
|
33
87
|
guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { return false }
|
|
34
|
-
guard let
|
|
35
|
-
return Date() <
|
|
88
|
+
guard let exp = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
|
|
89
|
+
return Date() < exp
|
|
36
90
|
}
|
|
37
91
|
|
|
38
|
-
|
|
39
|
-
let appName = application.localizedDisplayName ?? "This app"
|
|
40
|
-
|
|
41
|
-
// If temporarily unlocked, show a different message
|
|
92
|
+
private func buildConfig(appName: String) -> ShieldConfiguration {
|
|
42
93
|
if isTemporarilyUnlocked() {
|
|
43
94
|
return ShieldConfiguration(
|
|
44
|
-
backgroundBlurStyle:
|
|
45
|
-
backgroundColor: nil,
|
|
95
|
+
backgroundBlurStyle: resolveBlurStyle(cfgBlurStyle),
|
|
96
|
+
backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
|
|
46
97
|
icon: mascotIcon,
|
|
47
|
-
title: ShieldConfiguration.Label(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
),
|
|
51
|
-
subtitle: ShieldConfiguration.Label(
|
|
52
|
-
text: "Your free time is loading. Try again in a moment.",
|
|
53
|
-
color: subtitleGray
|
|
54
|
-
),
|
|
55
|
-
primaryButtonLabel: ShieldConfiguration.Label(
|
|
56
|
-
text: "OK",
|
|
57
|
-
color: .white
|
|
58
|
-
),
|
|
59
|
-
primaryButtonBackgroundColor: primaryGreen,
|
|
98
|
+
title: ShieldConfiguration.Label(text: "Almost there!", color: hexColor(cfgTitleColor)),
|
|
99
|
+
subtitle: ShieldConfiguration.Label(text: "Your free time is loading. Try again in a moment.", color: hexColor(cfgSubtitleColor)),
|
|
100
|
+
primaryButtonLabel: ShieldConfiguration.Label(text: "OK", color: .white),
|
|
101
|
+
primaryButtonBackgroundColor: hexColor(cfgPrimaryBgColor),
|
|
60
102
|
secondaryButtonLabel: nil
|
|
61
103
|
)
|
|
62
104
|
}
|
|
63
105
|
|
|
106
|
+
let title = cfgTitle.replacingOccurrences(of: "{appName}", with: appName)
|
|
107
|
+
let subtitle = cfgSubtitle.replacingOccurrences(of: "{appName}", with: appName)
|
|
64
108
|
let count = getBlockedAppCount()
|
|
65
|
-
let
|
|
66
|
-
? "You have \(count) apps blocked.
|
|
67
|
-
:
|
|
109
|
+
let fullSubtitle = count > 1
|
|
110
|
+
? "\(subtitle) You have \(count) apps blocked."
|
|
111
|
+
: subtitle
|
|
112
|
+
|
|
113
|
+
let secondaryLabel: ShieldConfiguration.Label? =
|
|
114
|
+
cfgSecondaryLabel == "NONE" ? nil :
|
|
115
|
+
ShieldConfiguration.Label(text: cfgSecondaryLabel, color: hexColor(cfgSecondaryLabelColor))
|
|
68
116
|
|
|
69
117
|
return ShieldConfiguration(
|
|
70
|
-
backgroundBlurStyle:
|
|
71
|
-
backgroundColor: nil,
|
|
118
|
+
backgroundBlurStyle: resolveBlurStyle(cfgBlurStyle),
|
|
119
|
+
backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
|
|
72
120
|
icon: mascotIcon,
|
|
73
|
-
title: ShieldConfiguration.Label(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
),
|
|
77
|
-
|
|
78
|
-
text: "\(appName) is blocked. \(contextLine)",
|
|
79
|
-
color: subtitleGray
|
|
80
|
-
),
|
|
81
|
-
primaryButtonLabel: ShieldConfiguration.Label(
|
|
82
|
-
text: "Earn Free Time",
|
|
83
|
-
color: .white
|
|
84
|
-
),
|
|
85
|
-
primaryButtonBackgroundColor: accentOrange,
|
|
86
|
-
secondaryButtonLabel: ShieldConfiguration.Label(
|
|
87
|
-
text: "Not now",
|
|
88
|
-
color: subtitleGray
|
|
89
|
-
)
|
|
121
|
+
title: ShieldConfiguration.Label(text: title, color: hexColor(cfgTitleColor)),
|
|
122
|
+
subtitle: ShieldConfiguration.Label(text: fullSubtitle, color: hexColor(cfgSubtitleColor)),
|
|
123
|
+
primaryButtonLabel: ShieldConfiguration.Label(text: cfgPrimaryLabel, color: hexColor(cfgPrimaryLabelColor)),
|
|
124
|
+
primaryButtonBackgroundColor: hexColor(cfgPrimaryBgColor),
|
|
125
|
+
secondaryButtonLabel: secondaryLabel
|
|
90
126
|
)
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
|
|
94
|
-
in category: ActivityCategory) -> ShieldConfiguration {
|
|
95
|
-
let categoryName = category.localizedDisplayName ?? "This category"
|
|
129
|
+
// ── Overrides ─────────────────────────────────────────────────────────
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
backgroundColor: nil,
|
|
100
|
-
icon: mascotIcon,
|
|
101
|
-
title: ShieldConfiguration.Label(
|
|
102
|
-
text: "Hold on!",
|
|
103
|
-
color: darkText
|
|
104
|
-
),
|
|
105
|
-
subtitle: ShieldConfiguration.Label(
|
|
106
|
-
text: "\(categoryName) is blocked. Take a quick quiz to earn free time!",
|
|
107
|
-
color: subtitleGray
|
|
108
|
-
),
|
|
109
|
-
primaryButtonLabel: ShieldConfiguration.Label(
|
|
110
|
-
text: "Earn Free Time",
|
|
111
|
-
color: .white
|
|
112
|
-
),
|
|
113
|
-
primaryButtonBackgroundColor: accentOrange,
|
|
114
|
-
secondaryButtonLabel: ShieldConfiguration.Label(
|
|
115
|
-
text: "Not now",
|
|
116
|
-
color: subtitleGray
|
|
117
|
-
)
|
|
118
|
-
)
|
|
131
|
+
override func configuration(shielding application: Application) -> ShieldConfiguration {
|
|
132
|
+
buildConfig(appName: application.localizedDisplayName ?? "This app")
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
override func configuration(shielding
|
|
122
|
-
|
|
135
|
+
override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
|
|
136
|
+
buildConfig(appName: category.localizedDisplayName ?? "This category")
|
|
137
|
+
}
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
backgroundColor: nil,
|
|
127
|
-
icon: mascotIcon,
|
|
128
|
-
title: ShieldConfiguration.Label(
|
|
129
|
-
text: "Hold on!",
|
|
130
|
-
color: darkText
|
|
131
|
-
),
|
|
132
|
-
subtitle: ShieldConfiguration.Label(
|
|
133
|
-
text: "\(domain) is blocked. Take a quick quiz to earn free time!",
|
|
134
|
-
color: subtitleGray
|
|
135
|
-
),
|
|
136
|
-
primaryButtonLabel: ShieldConfiguration.Label(
|
|
137
|
-
text: "Earn Free Time",
|
|
138
|
-
color: .white
|
|
139
|
-
),
|
|
140
|
-
primaryButtonBackgroundColor: accentOrange,
|
|
141
|
-
secondaryButtonLabel: ShieldConfiguration.Label(
|
|
142
|
-
text: "Not now",
|
|
143
|
-
color: subtitleGray
|
|
144
|
-
)
|
|
145
|
-
)
|
|
139
|
+
override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
|
|
140
|
+
buildConfig(appName: webDomain.domain ?? "This website")
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
override func configuration(shielding webDomain: WebDomain,
|
|
149
|
-
|
|
150
|
-
return configuration(shielding: webDomain)
|
|
143
|
+
override func configuration(shielding webDomain: WebDomain, in category: ActivityCategory) -> ShieldConfiguration {
|
|
144
|
+
buildConfig(appName: webDomain.domain ?? "This website")
|
|
151
145
|
}
|
|
152
146
|
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
-
|
|
5
|
-
Pod::Spec.new do |s|
|
|
6
|
-
s.name = 'ExpoAppBlocker'
|
|
7
|
-
s.version = package['version']
|
|
8
|
-
s.summary = package['description']
|
|
9
|
-
s.description = package['description']
|
|
10
|
-
s.license = package['license']
|
|
11
|
-
s.author = package['author'] || 'expo-app-blocker contributors'
|
|
12
|
-
s.homepage = package['homepage']
|
|
13
|
-
s.platforms = { :ios => '16.0' }
|
|
14
|
-
s.swift_version = '5.9'
|
|
15
|
-
s.source = { git: '' }
|
|
16
|
-
s.static_framework = true
|
|
17
|
-
|
|
18
|
-
s.dependency 'ExpoModulesCore'
|
|
19
|
-
|
|
20
|
-
s.source_files = '**/*.{h,m,swift}'
|
|
21
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
// This file provides default configuration values.
|
|
4
|
-
// The actual values are injected by the config plugin at prebuild time
|
|
5
|
-
// into a generated file in the app's ios directory.
|
|
6
|
-
// If the generated file exists, its values override these defaults.
|
|
7
|
-
|
|
8
|
-
public struct ExpoAppBlockerConfig {
|
|
9
|
-
// Override this in your app by creating a file with:
|
|
10
|
-
// let expoAppBlockerAppGroup = "group.com.yourapp.blocker"
|
|
11
|
-
public static var appGroupIdentifier: String {
|
|
12
|
-
// Try to read from UserDefaults (set by config plugin)
|
|
13
|
-
if let appGroup = UserDefaults.standard.string(forKey: "expo.appblocker.appGroup") {
|
|
14
|
-
return appGroup
|
|
15
|
-
}
|
|
16
|
-
// Fallback - should be overridden
|
|
17
|
-
return "group.expo.app-blocker"
|
|
18
|
-
}
|
|
19
|
-
}
|