It was commented out wholesale in 398b7b9 so the App-Store/TestFlight provisioning profile would build (Apple hasn't granted the *Distribution* Family Controls entitlement yet). But that also killed it for the dev-client, so denyAppRemoval / ManagedSettings throws "NSCocoaErrorDomain:4099 — can't talk to the helper app" when you flip the Blocker-page App-Lock. Gate it on REBREAK_ENABLE_FAMILY_CONTROLS, set to "1" in eas.json's development profile (internal distribution → Development entitlement, which we do have). The preview/production profiles stay without it until Apple approves the Distribution entitlement — then add the flag there too + bump buildNumber. NOTE: the next `eas build -p ios --profile development` will re-provision the main app profile to include the entitlement; if Apple turns out NOT to have granted the *Development* one either, that build will fail the same way the TestFlight one did. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
206 lines
8.0 KiB
JavaScript
206 lines
8.0 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
/**
|
|
* Expo Config-Plugin — wires the NEFilter Extension target into the iOS
|
|
* project at prebuild time.
|
|
*
|
|
* Was es macht:
|
|
* 1) Setzt die Entitlements der Haupt-App (family-controls, network-
|
|
* extension, app-groups).
|
|
* 2) Kopiert `modules/rebreak-protection/ios/RebreakURLFilter/` nach
|
|
* `ios/RebreakURLFilter/` (idempotent).
|
|
* 3) Fügt einen neuen Xcode-Target `RebreakURLFilter` (Bundle-ID
|
|
* `org.rebreak.app.RebreakURLFilter`) zum Projekt hinzu, mit:
|
|
* - Source-File: FilterControlProvider.swift
|
|
* - NetworkExtension.framework
|
|
* - Embed-App-Extensions Build-Phase im Haupt-Target
|
|
* - Entitlements via `RebreakURLFilter.entitlements`
|
|
*
|
|
* Wird aus app.config.ts via `plugins: ['./plugins/with-rebreak-protection-ios']`
|
|
* registriert. Idempotent — kann beliebig oft via `expo prebuild` laufen.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const {
|
|
withEntitlementsPlist,
|
|
withDangerousMod,
|
|
withXcodeProject,
|
|
} = require('@expo/config-plugins');
|
|
|
|
const APP_GROUP = 'group.org.rebreak.app';
|
|
const TARGET_NAME = 'RebreakURLFilter';
|
|
const EXT_BUNDLE_SUFFIX = 'RebreakURLFilter';
|
|
const MODULE_DIR = path.join(
|
|
__dirname,
|
|
'..',
|
|
'modules',
|
|
'rebreak-protection',
|
|
'ios',
|
|
TARGET_NAME,
|
|
);
|
|
|
|
// ─── 1) Haupt-App Entitlements ──────────────────────────────────────────────
|
|
|
|
function withMainAppEntitlements(config) {
|
|
return withEntitlementsPlist(config, (cfg) => {
|
|
cfg.modResults['com.apple.developer.networking.networkextension'] = [
|
|
'content-filter-provider',
|
|
];
|
|
// Family Controls: das DISTRIBUTION-Entitlement liegt noch bei Apple zur Freigabe
|
|
// → AppStore/TestFlight-Builds (Profile preview/production) dürfen es NICHT claimen,
|
|
// sonst kippt EAS' Provisioning ("doesn't support the Family Controls capability").
|
|
// Für development-Builds (Dev-Entitlement, internal distribution) schalten wir's per
|
|
// Env-Flag wieder ein (eas.json → development.env.REBREAK_ENABLE_FAMILY_CONTROLS="1"),
|
|
// damit denyAppRemoval / ManagedSettings im Dev-Client testbar ist. Sobald Apple das
|
|
// Distribution-Entitlement freigibt: Flag auch in preview/production env setzen + buildNumber bump.
|
|
if (process.env.REBREAK_ENABLE_FAMILY_CONTROLS === '1') {
|
|
cfg.modResults['com.apple.developer.family-controls'] = true;
|
|
}
|
|
const groups = cfg.modResults['com.apple.security.application-groups'] || [];
|
|
if (!groups.includes(APP_GROUP)) {
|
|
cfg.modResults['com.apple.security.application-groups'] = [...groups, APP_GROUP];
|
|
}
|
|
return cfg;
|
|
});
|
|
}
|
|
|
|
// ─── 2) Extension-Sources ins ios/-Verzeichnis kopieren ─────────────────────
|
|
|
|
function withCopyExtensionSources(config) {
|
|
return withDangerousMod(config, [
|
|
'ios',
|
|
async (cfg) => {
|
|
const platformProjectRoot = cfg.modRequest.platformProjectRoot;
|
|
const dest = path.join(platformProjectRoot, TARGET_NAME);
|
|
if (!fs.existsSync(MODULE_DIR)) {
|
|
throw new Error(
|
|
`[with-rebreak-protection-ios] Extension source dir missing: ${MODULE_DIR}`,
|
|
);
|
|
}
|
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
for (const file of fs.readdirSync(MODULE_DIR)) {
|
|
const srcFile = path.join(MODULE_DIR, file);
|
|
const destFile = path.join(dest, file);
|
|
fs.copyFileSync(srcFile, destFile);
|
|
}
|
|
return cfg;
|
|
},
|
|
]);
|
|
}
|
|
|
|
// ─── 3) Xcode-Target hinzufügen ──────────────────────────────────────────────
|
|
|
|
function withExtensionTarget(config) {
|
|
return withXcodeProject(config, async (cfg) => {
|
|
const proj = cfg.modResults;
|
|
|
|
// Idempotenz: skip wenn Target schon angelegt
|
|
if (proj.pbxTargetByName(TARGET_NAME)) {
|
|
return cfg;
|
|
}
|
|
|
|
const mainBundleId = cfg.ios?.bundleIdentifier;
|
|
if (!mainBundleId) {
|
|
throw new Error('[with-rebreak-protection-ios] ios.bundleIdentifier fehlt in app.config');
|
|
}
|
|
const extBundleId = `${mainBundleId}.${EXT_BUNDLE_SUFFIX}`;
|
|
|
|
// ── Target anlegen (Type: app_extension) ──
|
|
const target = proj.addTarget(TARGET_NAME, 'app_extension', TARGET_NAME, extBundleId);
|
|
|
|
// ── Build-Phasen: Sources + Frameworks + Resources ──
|
|
proj.addBuildPhase(
|
|
['FilterControlProvider.swift'],
|
|
'PBXSourcesBuildPhase',
|
|
'Sources',
|
|
target.uuid,
|
|
);
|
|
proj.addBuildPhase(
|
|
['NetworkExtension.framework'],
|
|
'PBXFrameworksBuildPhase',
|
|
'Frameworks',
|
|
target.uuid,
|
|
);
|
|
// Info.plist gehört NICHT als Resource — wird via INFOPLIST_FILE referenziert.
|
|
|
|
// ── PBXGroup für die Sources ──
|
|
const pbxGroup = proj.addPbxGroup(
|
|
['FilterControlProvider.swift', 'Info.plist', 'RebreakURLFilter.entitlements'],
|
|
TARGET_NAME,
|
|
TARGET_NAME,
|
|
);
|
|
// Group ans CustomTemplate-Group hängen damit sie im Project Navigator erscheint
|
|
const groups = proj.hash.project.objects.PBXGroup;
|
|
Object.keys(groups).forEach((key) => {
|
|
if (
|
|
groups[key].name === 'CustomTemplate' ||
|
|
(groups[key].name === undefined && groups[key].path === undefined)
|
|
) {
|
|
proj.addToPbxGroup(pbxGroup.uuid, key);
|
|
}
|
|
});
|
|
|
|
// ── Build-Settings auf der Target-Configuration anpassen ──
|
|
const configurations = proj.pbxXCBuildConfigurationSection();
|
|
Object.keys(configurations)
|
|
.filter((k) => typeof configurations[k] === 'object')
|
|
.forEach((k) => {
|
|
const buildSettingsObj = configurations[k].buildSettings;
|
|
if (
|
|
buildSettingsObj &&
|
|
buildSettingsObj.PRODUCT_NAME &&
|
|
buildSettingsObj.PRODUCT_NAME.replace(/"/g, '') === TARGET_NAME
|
|
) {
|
|
buildSettingsObj.INFOPLIST_FILE = `"${TARGET_NAME}/Info.plist"`;
|
|
buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${TARGET_NAME}/${TARGET_NAME}.entitlements"`;
|
|
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '15.1';
|
|
buildSettingsObj.SWIFT_VERSION = '5.9';
|
|
buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"';
|
|
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic';
|
|
// EAS managed credentials setzen DEVELOPMENT_TEAM nur auf der Main-App.
|
|
// Die Extension ist ein eigenes Target → muss expliziten Team-Wert haben,
|
|
// sonst: "Signing for 'RebreakURLFilter' requires a development team".
|
|
buildSettingsObj.DEVELOPMENT_TEAM = '84BQ7MTFYK';
|
|
}
|
|
});
|
|
|
|
// ── Embed App Extensions Build-Phase im Haupt-Target ──
|
|
// Suche nach existierender CopyFilesBuildPhase mit Comment "Embed App Extensions"
|
|
const mainTargetUuid = proj.getFirstTarget().uuid;
|
|
const buildPhases = proj.hash.project.objects.PBXNativeTarget[mainTargetUuid].buildPhases;
|
|
const copyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {};
|
|
const hasEmbedPhase = Object.keys(copyFilesPhases).some((key) => {
|
|
const phase = copyFilesPhases[key];
|
|
return (
|
|
typeof phase === 'object' &&
|
|
phase.dstSubfolderSpec === 13 && // 13 = PluginsAndFrameworks (App Extensions)
|
|
buildPhases.some((bp) => bp.value === key)
|
|
);
|
|
});
|
|
if (!hasEmbedPhase) {
|
|
proj.addBuildPhase(
|
|
[`${TARGET_NAME}.appex`],
|
|
'PBXCopyFilesBuildPhase',
|
|
'Embed App Extensions',
|
|
mainTargetUuid,
|
|
'app_extension', // dstSubfolderSpec=13
|
|
);
|
|
}
|
|
|
|
// ── Target-Dependency: Haupt-App muss Extension vor sich bauen ──
|
|
proj.addTargetDependency(mainTargetUuid, [target.uuid]);
|
|
|
|
return cfg;
|
|
});
|
|
}
|
|
|
|
// ─── Composition ────────────────────────────────────────────────────────────
|
|
|
|
module.exports = function withRebreakProtectionIos(config) {
|
|
config = withMainAppEntitlements(config);
|
|
config = withCopyExtensionSources(config);
|
|
config = withExtensionTarget(config);
|
|
return config;
|
|
};
|