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.
@@ -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 overlay title |
89
- | `ios.shield.subtitle` | `string` | `"{appName} is blocked."` | Shield subtitle. `{appName}` is replaced with the blocked app name |
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.primaryButtonColor` | `string` | `"#7cb518"` | Primary button background color (hex) |
93
- | `ios.shield.backgroundBlurStyle` | `string` | `"systemThickMaterial"` | iOS blur style |
94
- | `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (relative to project root, e.g. `"./assets/shield-icon.png"`) |
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.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",
@@ -141,7 +141,23 @@ function withAppBlockerIOS(config, pluginConfig) {
141
141
  }
142
142
  }
143
143
 
144
- // Inject app group into extension Swift files
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
- if (content.includes("APP_GROUP_PLACEHOLDER")) {
157
- content = content.replace(/APP_GROUP_PLACEHOLDER/g, appGroup);
158
- fs.writeFileSync(filePath, content);
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 = pluginConfig?.ios?.shield?.icon;
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 shown on the shield. Use {appName} as placeholder. Default: "Hold on!" */
69
+ /** Title text. Use {appName} as placeholder for the blocked app name. Default: "Hold on!" */
70
70
  title?: string;
71
- /** Subtitle shown on the shield. Use {appName} as placeholder. */
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
- /** Primary button label. Default: "Earn Free Time" */
75
+ /** Subtitle text color (hex). Default: "#8c8c8c" */
76
+ subtitleColor?: string;
77
+ /** Primary button label text. Default: "Earn Free Time" */
74
78
  primaryButtonLabel?: string;
75
- /** Secondary button label. Set to null to hide. Default: "Not now" */
76
- secondaryButtonLabel?: string | null;
79
+ /** Primary button label text color (hex). Default: "#ffffff" */
80
+ primaryButtonLabelColor?: string;
77
81
  /** Primary button background color (hex). Default: "#7cb518" */
78
- primaryButtonColor?: string;
79
- /** Background color (hex). Default: null (uses blur) */
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
- /** Background blur style. Default: "systemThickMaterial" */
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 image. Optional. */
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
- // Grandmizer design system colors
10
- private let primaryGreen = UIColor(red: 0.486, green: 0.710, blue: 0.094, alpha: 1.0) // #7cb518
11
- private let darkGreen = UIColor(red: 0.361, green: 0.502, blue: 0.004, alpha: 1.0) // #5c8001
12
- private let accentOrange = UIColor(red: 0.984, green: 0.380, blue: 0.027, alpha: 1.0) // #fb6107
13
- private let darkText = UIColor(red: 0.067, green: 0.067, blue: 0.067, alpha: 1.0) // #111111
14
- private let subtitleGray = UIColor(red: 0.45, green: 0.45, blue: 0.45, alpha: 1.0)
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 expiration = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
35
- return Date() < expiration
88
+ guard let exp = defaults.object(forKey: "appBlocker.temporaryUnlock.v1") as? Date else { return false }
89
+ return Date() < exp
36
90
  }
37
91
 
38
- override func configuration(shielding application: Application) -> ShieldConfiguration {
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: .systemThickMaterial,
45
- backgroundColor: nil,
95
+ backgroundBlurStyle: resolveBlurStyle(cfgBlurStyle),
96
+ backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
46
97
  icon: mascotIcon,
47
- title: ShieldConfiguration.Label(
48
- text: "Almost there!",
49
- color: darkText
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 contextLine = count > 1
66
- ? "You have \(count) apps blocked. Stay focused!"
67
- : "Stay focused! Take a quick quiz to earn free time."
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: .systemThickMaterial,
71
- backgroundColor: nil,
118
+ backgroundBlurStyle: resolveBlurStyle(cfgBlurStyle),
119
+ backgroundColor: cfgBgColor == "NONE" ? nil : hexColor(cfgBgColor),
72
120
  icon: mascotIcon,
73
- title: ShieldConfiguration.Label(
74
- text: "Hold on!",
75
- color: darkText
76
- ),
77
- subtitle: ShieldConfiguration.Label(
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
- override func configuration(shielding application: Application,
94
- in category: ActivityCategory) -> ShieldConfiguration {
95
- let categoryName = category.localizedDisplayName ?? "This category"
129
+ // ── Overrides ─────────────────────────────────────────────────────────
96
130
 
97
- return ShieldConfiguration(
98
- backgroundBlurStyle: .systemThickMaterial,
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 webDomain: WebDomain) -> ShieldConfiguration {
122
- let domain = webDomain.domain ?? "This website"
135
+ override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
136
+ buildConfig(appName: category.localizedDisplayName ?? "This category")
137
+ }
123
138
 
124
- return ShieldConfiguration(
125
- backgroundBlurStyle: .systemThickMaterial,
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
- in category: ActivityCategory) -> ShieldConfiguration {
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
- }