/* 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', ]; // TEMP: Family Controls Distribution Entitlement liegt bei Apple zur Freigabe. // Solange das Antrag nicht durch ist, kann EAS kein AppStore-Provisioning-Profil // mit FC erstellen → Build-Fehler. Sobald Apple freigibt: Zeile wieder rein // (oder via Env-Flag) + buildNumber bump. // 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; };