rebreak-monorepo/apps/rebreak-native/plugins/with-rebreak-protection-ios.js
chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

611 lines
24 KiB
JavaScript

/* eslint-disable @typescript-eslint/no-var-requires */
/**
* Expo Config-Plugin — bindet das NEURLFilter-ExtensionKit-Target
* (`RebreakURLFilterExtension`) beim Prebuild ins iOS-Projekt ein.
*
* Ersetzt das frühere NEFilter-Plugin. Erkenntnis aus Apples `SimpleURLFilter`-
* Sample: das Target ist KEIN exotischer Produkttyp — es bleibt das klassische
* `app_extension`. Die ExtensionKit-Natur kommt aus:
* 1. der Info.plist (`EXAppExtensionAttributes` / `EXExtensionPointIdentifier`)
* 2. der Embed-Phase „Embed Foundation Extensions" mit `dstSubfolderSpec = 16`
* (das klassische „Embed App Extensions" wäre 13).
*
* Was es macht:
* 1) Haupt-App-Entitlements (url-filter-provider, family-controls, app-groups)
* 2) kopiert `modules/rebreak-protection/ios/RebreakURLFilterExtension/` → `ios/`
* 3) fügt das Xcode-Target `RebreakURLFilterExtension` hinzu
*
* 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 = 'RebreakURLFilterExtension';
const EXT_BUNDLE_SUFFIX = 'URLFilterExtension'; // → org.rebreak.app.URLFilterExtension
const DEVELOPMENT_TEAM = '84BQ7MTFYK';
const MODULE_DIR = path.join(
__dirname,
'..',
'modules',
'rebreak-protection',
'ios',
TARGET_NAME,
);
// Swift-Quellen (Apple-Bloom vendored + unser Control-Provider).
const SWIFT_SOURCES = [
'BloomFilter.swift',
'Murmur3Hash.swift',
'FNV1aHash.swift',
'RebreakURLFilterControlProvider.swift',
];
// Bundle-Resources.
const RESOURCES = ['bloom_filter.plist'];
// ─── Packet-Tunnel-Extension (Layer 1 — Default-Filter) ──────────────────────
// Neues Target: der NEPacketTunnelProvider-DNS-Sinkhole. Ersetzt NEURLFilter
// als primären, lieferbaren iOS-Filter. KLASSISCHE PluginKit-App-Extension —
// normaler `app_extension`-Pfad, normale „Embed App Extensions"-Phase
// (dstSubfolderSpec 13). KEIN ExtensionKit-Sonderweg.
const PT_TARGET_NAME = 'RebreakPacketTunnelExtension';
const PT_BUNDLE_SUFFIX = 'PacketTunnelExtension'; // → org.rebreak.app.PacketTunnelExtension
const PT_MODULE_DIR = path.join(
__dirname,
'..',
'modules',
'rebreak-protection',
'ios',
PT_TARGET_NAME,
);
// Swift-Quellen (Provider + Filter-Logik, aus Android nach Swift portiert).
const PT_SWIFT_SOURCES = [
'PacketTunnelProvider.swift',
'DnsFilter.swift',
'HashList.swift',
'DomainHasher.swift',
];
// ─── Content-Filter-Extension (Layer 1 — Supervised-Pfad) ────────────────────
// Klassischer NEFilterDataProvider — wiederbelebt aus a80cc8b. Wird auf
// supervised-MDM-Geräten als Layer 1 statt PacketTunnel aktiviert (kein VPN-
// Toggle für den User). Dynamische Hash-Liste aus App-Group blocklist.bin.
// KEIN PIR-Server, KEIN ExtensionKit-Sonderweg — klassische dst-13-PlugIns/.
const CF_TARGET_NAME = 'RebreakContentFilter';
const CF_BUNDLE_SUFFIX = 'ContentFilterExtension'; // → org.rebreak.app.ContentFilterExtension
const CF_MODULE_DIR = path.join(
__dirname,
'..',
'modules',
'rebreak-protection',
'ios',
CF_TARGET_NAME,
);
const CF_SWIFT_SOURCES = ['FilterDataProvider.swift'];
// ─── 1) Haupt-App-Entitlements ──────────────────────────────────────────────
function withMainAppEntitlements(config) {
return withEntitlementsPlist(config, (cfg) => {
// NetworkExtension-Entitlement.
// - packet-tunnel-provider: PacketTunnel-DNS-Sinkhole (unsupervised-Pfad,
// User-toggleable VPN)
// - content-filter-provider: klassisches NEFilter (supervised-Pfad, kein
// User-Toggle in Settings)
// App entscheidet zur Laufzeit per isMdmSupervised welcher Stack aktiviert
// wird — beide Entitlements müssen verfügbar sein.
cfg.modResults['com.apple.developer.networking.networkextension'] = [
'packet-tunnel-provider',
'content-filter-provider',
];
// Family Controls = Kern-Funktion, Apple-Distribution-Entitlement freigegeben
// (v0.3.4) → DEFAULT AN. Nur ein explizites REBREAK_ENABLE_FAMILY_CONTROLS=0
// schaltet es ab (Escape-Hatch). Muss identisch zu app.config.ts
// `familyControlsEnabled` bleiben.
if (process.env.REBREAK_ENABLE_FAMILY_CONTROLS !== '0') {
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) => {
// Extension-Verzeichnis(se) nach ios/ kopieren.
for (const [target, srcDir] of [
[PT_TARGET_NAME, PT_MODULE_DIR],
[CF_TARGET_NAME, CF_MODULE_DIR],
]) {
const dest = path.join(cfg.modRequest.platformProjectRoot, target);
if (!fs.existsSync(srcDir)) {
throw new Error(
`[with-rebreak-protection-ios] Extension source dir missing: ${srcDir}`,
);
}
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const file of fs.readdirSync(srcDir)) {
fs.copyFileSync(path.join(srcDir, file), path.join(dest, file));
}
}
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 (Produkttyp app_extension — wie Apples SimpleURLFilter) ──
const target = proj.addTarget(TARGET_NAME, 'app_extension', TARGET_NAME, extBundleId);
// ── Build-Phasen ──
proj.addBuildPhase(SWIFT_SOURCES, 'PBXSourcesBuildPhase', 'Sources', target.uuid);
proj.addBuildPhase(RESOURCES, 'PBXResourcesBuildPhase', 'Resources', target.uuid);
proj.addBuildPhase(
['NetworkExtension.framework'],
'PBXFrameworksBuildPhase',
'Frameworks',
target.uuid,
);
// ── PBXGroup für die Extension-Dateien ──
const pbxGroup = proj.addPbxGroup(
[...SWIFT_SOURCES, ...RESOURCES, 'Info.plist', `${TARGET_NAME}.entitlements`],
TARGET_NAME,
TARGET_NAME,
);
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 ──
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"`;
// NEURLFilter ist iOS 26+. Die Extension lädt nur dort — höheres
// Deployment-Target als die Haupt-App (iOS 16+) ist korrekt.
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '26.0';
buildSettingsObj.SWIFT_VERSION = '5.0';
buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"';
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic';
// EAS managed credentials setzen DEVELOPMENT_TEAM nur auf der Main-App
// → Extension braucht expliziten Team-Wert.
buildSettingsObj.DEVELOPMENT_TEAM = DEVELOPMENT_TEAM;
}
});
// ── Embed-Phase auf ExtensionKit umstellen ──
// proj.addTarget() hat bereits eine „Copy Files"-PBXCopyFilesBuildPhase
// (dstSubfolderSpec 13 = PlugIns/) im Haupt-Target angelegt, die unsere
// .appex einbettet. iOS' Installer (MIPluginKitBundle) behandelt aber
// ALLES in PlugIns/ als klassische PluginKit-Extension und verlangt ein
// NSExtension-Dict im Info.plist — das ExtensionKit-Extensions NICHT haben
// ("AppexBundleMissingNSExtensionDict", MIInstallerError 39). ExtensionKit-
// Extensions MÜSSEN nach Extensions/. Also die bestehende Phase umbiegen:
// dstSubfolderSpec 16 + dstPath $(EXTENSIONS_FOLDER_PATH) — exakt wie
// Apples SimpleURLFilter.xcodeproj. KEINE zweite Phase anlegen (sonst wird
// die .appex doppelt eingebettet).
const mainTargetUuid = proj.getFirstTarget().uuid;
const copyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {};
let embedFixed = false;
Object.keys(copyFilesPhases).forEach((key) => {
const phase = copyFilesPhases[key];
if (typeof phase !== 'object') return;
const embedsOurAppex = (phase.files || []).some(
(f) => typeof f?.comment === 'string' && f.comment.includes(`${TARGET_NAME}.appex`),
);
if (!embedsOurAppex) return;
phase.name = '"Embed Foundation Extensions"';
phase.dstSubfolderSpec = 16;
phase.dstPath = '"$(EXTENSIONS_FOLDER_PATH)"';
embedFixed = true;
});
if (!embedFixed) {
throw new Error(
'[with-rebreak-protection-ios] Embed-CopyFiles-Phase für die .appex nicht gefunden',
);
}
// ── Target-Dependency: Haupt-App baut die Extension vorher ──
proj.addTargetDependency(mainTargetUuid, [target.uuid]);
return cfg;
});
}
// ─── 4) Packet-Tunnel-Xcode-Target hinzufügen ────────────────────────────────
//
// KLASSISCHE App-Extension — anders als das NEURLFilter-Target:
// - normaler `app_extension`-Produkttyp,
// - „Embed App Extensions"-Phase mit dstSubfolderSpec 13 (PlugIns/),
// - KEIN ExtensionKit-Sonderweg (kein dstSubfolderSpec 16, kein
// EXAppExtensionAttributes).
// - Deployment-Target = Main-App (iOS 16), KEIN 26.0-Gate.
//
// ACHTUNG: `addTarget` legt zwar eine korrekte dst-13-Phase an, embedded die
// .appex aber wegen einer Eigenheit der `xcode`-Lib in die NEURLFilter-dst-16-
// Phase (siehe ausführliche Begründung am Embed-Phase-Fix unten). Darum MUSS
// die .appex hier explizit in die eigene dst-13-Phase umgehängt werden.
function withPacketTunnelTarget(config) {
return withXcodeProject(config, async (cfg) => {
const proj = cfg.modResults;
// Idempotenz: skip wenn Target schon angelegt.
if (proj.pbxTargetByName(PT_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}.${PT_BUNDLE_SUFFIX}`;
// ── Target anlegen (klassischer app_extension-Produkttyp) ──
const target = proj.addTarget(
PT_TARGET_NAME,
'app_extension',
PT_TARGET_NAME,
extBundleId,
);
// ── Build-Phasen ──
proj.addBuildPhase(
PT_SWIFT_SOURCES,
'PBXSourcesBuildPhase',
'Sources',
target.uuid,
);
proj.addBuildPhase(
['NetworkExtension.framework'],
'PBXFrameworksBuildPhase',
'Frameworks',
target.uuid,
);
// ── PBXGroup für die Extension-Dateien ──
const pbxGroup = proj.addPbxGroup(
[...PT_SWIFT_SOURCES, 'Info.plist', `${PT_TARGET_NAME}.entitlements`],
PT_TARGET_NAME,
PT_TARGET_NAME,
);
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 ──
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, '') === PT_TARGET_NAME
) {
buildSettingsObj.INFOPLIST_FILE = `"${PT_TARGET_NAME}/Info.plist"`;
buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${PT_TARGET_NAME}/${PT_TARGET_NAME}.entitlements"`;
// Packet-Tunnel läuft ab iOS 16 — Deployment-Target = Main-App.
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '16.0';
buildSettingsObj.SWIFT_VERSION = '5.0';
buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"';
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic';
// EAS managed credentials setzen DEVELOPMENT_TEAM nur auf der Main-App
// → Extension braucht expliziten Team-Wert.
buildSettingsObj.DEVELOPMENT_TEAM = DEVELOPMENT_TEAM;
}
});
// ── Embed-Phase korrigieren — die .appex MUSS allein in dst 13 (PlugIns/) ──
//
// BUG (verifiziert gegen node_modules/xcode/lib/pbxProject.js + einen
// End-to-End-Lauf der echten Plugin-Kette):
//
// 1. `withXcodeProject`-Mods laufen LIFO — `withPacketTunnelTarget` ist
// zuletzt registriert und läuft daher ZUERST, `withExtensionTarget`
// (NEURLFilter) DANACH.
// 2. `proj.addTarget('app_extension')` ruft intern (via `addProductFile`)
// `addToPbxCopyfilesBuildPhase`, BEVOR die neue dst-13-Copy-Files-Phase
// existiert. `buildPhaseObject` findet im neuen Target keine eigene
// Phase und fällt auf die ERSTE PBXCopyFilesBuildPhase zurück, deren
// SECTION-COMMENT exakt „Copy Files" ist — projektweit.
// 3. Ergebnis ohne Fix: die NEURLFilter-`addTarget` (läuft als zweites)
// biegt seine Phase per `phase.name`/`dstSubfolderSpec` auf dst 16 um,
// lässt den SECTION-COMMENT aber auf „Copy Files". Beim PacketTunnel-
// `addTarget` matcht `buildPhaseObject` diesen Comment und pusht die
// PacketTunnel-.appex in dieselbe dst-16-Phase. → .appex landet in
// `ReBreak.app/Extensions/` statt `PlugIns/` → Install-Crash
// `AppexBundleMissingEXAppExtensionAttributesDict` (klassische
// NSExtension im ExtensionKit-Ordner).
//
// Fix (zwei Teile, beide nötig):
// (a) Die PacketTunnel-.appex aus JEDER Copy-Files-Phase herausziehen und
// exklusiv in die eigene, von `addTarget` angelegte dst-13-Phase legen.
// (b) Den SECTION-COMMENT dieser dst-13-Phase von „Copy Files" auf
// „Embed App Extensions" umbenennen. SONST matcht der NACH uns laufende
// `withExtensionTarget`-`addTarget`-Aufruf via `buildPhaseObject(
// 'Copy Files')` unsere PacketTunnel-Phase und der dortige dst-16-Umbau
// zieht die PacketTunnel-.appex erneut mit. Comment-Rename entkoppelt
// beide Phasen sauber (NEURLFilters eigener Umbau matcht per
// .appex-Dateiname, nicht per Comment — bleibt also korrekt).
const ptCopyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {};
const ptPhaseKeys = Object.keys(ptCopyFilesPhases).filter(
(k) => typeof ptCopyFilesPhases[k] === 'object' && !/_comment$/.test(k),
);
// (a) Den PacketTunnel-.appex-BuildFile-Eintrag finden und aus ALLEN
// Copy-Files-Phasen entfernen.
let ptAppexBuildFile = null;
ptPhaseKeys.forEach((key) => {
const phase = ptCopyFilesPhases[key];
const kept = [];
(phase.files || []).forEach((f) => {
if (
typeof f?.comment === 'string' &&
f.comment.includes(`${PT_TARGET_NAME}.appex`)
) {
ptAppexBuildFile = f;
} else {
kept.push(f);
}
});
phase.files = kept;
});
if (!ptAppexBuildFile) {
throw new Error(
'[with-rebreak-protection-ios] PacketTunnel-.appex-BuildFile nicht gefunden',
);
}
// (b) Die von addTarget frisch angelegte (jetzt leere) dst-13-Phase finden,
// die .appex exklusiv dort einhängen UND den Section-Comment umbenennen.
// dst 13 = PlugIns/ = der korrekte Ort für eine klassische NSExtension-
// PluginKit-Extension.
const ptDst13Key = ptPhaseKeys.find(
(k) =>
ptCopyFilesPhases[k].dstSubfolderSpec === 13 &&
(ptCopyFilesPhases[k].files || []).length === 0,
);
if (!ptDst13Key) {
throw new Error(
'[with-rebreak-protection-ios] keine leere dst-13-Copy-Files-Phase für die PacketTunnel-.appex gefunden',
);
}
ptCopyFilesPhases[ptDst13Key].name = '"Embed App Extensions"';
ptCopyFilesPhases[ptDst13Key].files = [ptAppexBuildFile];
// Section-Comment-Key umbenennen — entkoppelt die Phase vom
// `buildPhaseObject('Copy Files')`-Lookup des NEURLFilter-Targets.
const ptDst13CommentKey = `${ptDst13Key}_comment`;
if (ptCopyFilesPhases[ptDst13CommentKey] === 'Copy Files') {
ptCopyFilesPhases[ptDst13CommentKey] = 'Embed App Extensions';
}
// Auch den Build-Phase-Verweis im Target auf den neuen Comment ziehen,
// damit pbxproj-Serialisierung + Xcode-Anzeige konsistent bleiben.
const ptNativeTargets = proj.hash.project.objects.PBXNativeTarget || {};
Object.keys(ptNativeTargets).forEach((tk) => {
const nt = ptNativeTargets[tk];
if (typeof nt !== 'object' || !Array.isArray(nt.buildPhases)) return;
nt.buildPhases.forEach((bp) => {
if (bp.value === ptDst13Key && bp.comment === 'Copy Files') {
bp.comment = 'Embed App Extensions';
}
});
});
// ── Target-Dependency: Haupt-App baut die Extension vorher ──
const mainTargetUuid = proj.getFirstTarget().uuid;
proj.addTargetDependency(mainTargetUuid, [target.uuid]);
return cfg;
});
}
// ─── 5) Content-Filter-Xcode-Target hinzufügen ───────────────────────────────
//
// 1:1 zum PacketTunnel-Hook: klassische app_extension, dst-13 PlugIns/,
// derselbe Embed-Phase-Fix (Comment-Rename, exklusive .appex-Zuweisung).
// LIFO-Composition: dieser Hook läuft NACH PacketTunnel — wenn PT seine Phase
// bereits umbenannt hat ("Embed App Extensions"), findet `addProductFile` keine
// "Copy Files"-Phase mehr und legt eine frische dst-13 für CF an, die wir hier
// dann gleich richtig setzen.
function withContentFilterTarget(config) {
return withXcodeProject(config, async (cfg) => {
const proj = cfg.modResults;
if (proj.pbxTargetByName(CF_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}.${CF_BUNDLE_SUFFIX}`;
const target = proj.addTarget(
CF_TARGET_NAME,
'app_extension',
CF_TARGET_NAME,
extBundleId,
);
proj.addBuildPhase(
CF_SWIFT_SOURCES,
'PBXSourcesBuildPhase',
'Sources',
target.uuid,
);
proj.addBuildPhase(
['NetworkExtension.framework'],
'PBXFrameworksBuildPhase',
'Frameworks',
target.uuid,
);
const pbxGroup = proj.addPbxGroup(
[...CF_SWIFT_SOURCES, 'Info.plist', `${CF_TARGET_NAME}.entitlements`],
CF_TARGET_NAME,
CF_TARGET_NAME,
);
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);
}
});
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, '') === CF_TARGET_NAME
) {
buildSettingsObj.INFOPLIST_FILE = `"${CF_TARGET_NAME}/Info.plist"`;
buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${CF_TARGET_NAME}/${CF_TARGET_NAME}.entitlements"`;
// NEFilterDataProvider gibt es seit iOS 9 — Deployment-Target = Main-App.
buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '16.0';
buildSettingsObj.SWIFT_VERSION = '5.0';
buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"';
buildSettingsObj.CODE_SIGN_STYLE = 'Automatic';
buildSettingsObj.DEVELOPMENT_TEAM = DEVELOPMENT_TEAM;
}
});
// Embed-Phase-Fix (siehe ausführliche Begründung im PacketTunnel-Hook).
// .appex aus allen Copy-Files-Phasen ziehen und exklusiv in eigene dst-13
// legen + Section-Comment auf "Embed App Extensions" umbenennen.
const cfCopyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {};
const cfPhaseKeys = Object.keys(cfCopyFilesPhases).filter(
(k) => typeof cfCopyFilesPhases[k] === 'object' && !/_comment$/.test(k),
);
let cfAppexBuildFile = null;
cfPhaseKeys.forEach((key) => {
const phase = cfCopyFilesPhases[key];
const kept = [];
(phase.files || []).forEach((f) => {
if (
typeof f?.comment === 'string' &&
f.comment.includes(`${CF_TARGET_NAME}.appex`)
) {
cfAppexBuildFile = f;
} else {
kept.push(f);
}
});
phase.files = kept;
});
if (!cfAppexBuildFile) {
throw new Error(
'[with-rebreak-protection-ios] ContentFilter-.appex-BuildFile nicht gefunden',
);
}
const cfDst13Key = cfPhaseKeys.find(
(k) =>
cfCopyFilesPhases[k].dstSubfolderSpec === 13 &&
(cfCopyFilesPhases[k].files || []).length === 0,
);
if (!cfDst13Key) {
throw new Error(
'[with-rebreak-protection-ios] keine leere dst-13-Copy-Files-Phase für die ContentFilter-.appex gefunden',
);
}
cfCopyFilesPhases[cfDst13Key].name = '"Embed App Extensions"';
cfCopyFilesPhases[cfDst13Key].files = [cfAppexBuildFile];
const cfDst13CommentKey = `${cfDst13Key}_comment`;
if (cfCopyFilesPhases[cfDst13CommentKey] === 'Copy Files') {
cfCopyFilesPhases[cfDst13CommentKey] = 'Embed App Extensions';
}
const cfNativeTargets = proj.hash.project.objects.PBXNativeTarget || {};
Object.keys(cfNativeTargets).forEach((tk) => {
const nt = cfNativeTargets[tk];
if (typeof nt !== 'object' || !Array.isArray(nt.buildPhases)) return;
nt.buildPhases.forEach((bp) => {
if (bp.value === cfDst13Key && bp.comment === 'Copy Files') {
bp.comment = 'Embed App Extensions';
}
});
});
const mainTargetUuid = proj.getFirstTarget().uuid;
proj.addTargetDependency(mainTargetUuid, [target.uuid]);
return cfg;
});
}
// ─── Composition ────────────────────────────────────────────────────────────
module.exports = function withRebreakProtectionIos(config) {
config = withMainAppEntitlements(config);
config = withCopyExtensionSources(config);
// withExtensionTarget (NEURLFilter, iOS 26 + PIR) entfernt — `url-filter-provider`
// ist kein gültiger EAS-Entitlement-Wert.
// ContentFilter (klassisches NEFilter) zuerst registrieren — läuft per LIFO
// NACH PacketTunnel, was dem Embed-Phase-Fix in beiden Hooks sauber entkoppelt.
config = withContentFilterTarget(config);
config = withPacketTunnelTarget(config);
return config;
};