Add 'apps/mobile/' from commit '5fa30f365f21531094cd4d2045042bb4f1370ac3'

git-subtree-dir: apps/mobile
git-subtree-mainline: 86f8987dff
git-subtree-split: 5fa30f365f
This commit is contained in:
2026-08-16 21:42:59 -04:00
222 changed files with 23436 additions and 0 deletions
@@ -0,0 +1,8 @@
import UIKit
enum BeenVoiceIntentHelpers {
@MainActor
static func openDeepLink(_ url: URL) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
@@ -0,0 +1,36 @@
import AppIntents
struct BeenVoiceShortcuts: AppShortcutsProvider {
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: ClockInIntent(),
phrases: [
"Clock in with \(.applicationName)",
"Start timer in \(.applicationName)",
"Start tracking time in \(.applicationName)",
],
shortTitle: "Clock In",
systemImageName: "play.circle.fill"
)
AppShortcut(
intent: ClockOutIntent(),
phrases: [
"Clock out in \(.applicationName)",
"Stop timer in \(.applicationName)",
"Stop tracking time in \(.applicationName)",
],
shortTitle: "Clock Out",
systemImageName: "stop.circle.fill"
)
AppShortcut(
intent: OpenTimerIntent(),
phrases: [
"Open time clock in \(.applicationName)",
"Open timer in \(.applicationName)",
],
shortTitle: "Time Clock",
systemImageName: "timer"
)
}
}
@@ -0,0 +1,31 @@
import AppIntents
struct ClockInIntent: AppIntent {
static var title: LocalizedStringResource = "Clock In"
static var description = IntentDescription("Start the beenvoice time clock with your last client.")
static var openAppWhenRun: Bool = true
static var isDiscoverable: Bool = true
@Parameter(title: "Title")
var title: String?
@MainActor
func perform() async throws -> some IntentResult {
var components = URLComponents()
components.scheme = "beenvoice"
components.host = "shortcuts"
components.path = "/clock-in"
if let title, !title.isEmpty {
components.queryItems = [URLQueryItem(name: "title", value: title)]
}
guard let url = components.url else {
return .result()
}
BeenVoiceIntentHelpers.openDeepLink(url)
return .result()
}
}
@@ -0,0 +1,19 @@
import AppIntents
struct ClockOutIntent: AppIntent {
static var title: LocalizedStringResource = "Clock Out"
static var description = IntentDescription("Stop the running beenvoice timer and save your time.")
static var openAppWhenRun: Bool = true
static var isDiscoverable: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
guard let url = URL(string: "beenvoice://shortcuts/clock-out") else {
return .result()
}
BeenVoiceIntentHelpers.openDeepLink(url)
return .result()
}
}
@@ -0,0 +1,19 @@
import AppIntents
struct OpenTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Open Time Clock"
static var description = IntentDescription("Open the beenvoice time clock.")
static var openAppWhenRun: Bool = true
static var isDiscoverable: Bool = true
@MainActor
func perform() async throws -> some IntentResult {
guard let url = URL(string: "beenvoice://timer") else {
return .result()
}
BeenVoiceIntentHelpers.openDeepLink(url)
return .result()
}
}
+143
View File
@@ -0,0 +1,143 @@
// @ts-check
const {
withDangerousMod,
withXcodeProject,
IOSConfig,
} = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
const SWIFT_FILES = [
"BeenVoiceIntentHelpers.swift",
"ClockInIntent.swift",
"ClockOutIntent.swift",
"OpenTimerIntent.swift",
"BeenVoiceShortcuts.swift",
];
const SHORTCUT_REGISTRATION = `Task {
await BeenVoiceShortcuts.updateAppShortcutParameters()
}`;
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withAppIntents(config) {
const appIntentsSource = path.join(
config._internal?.projectRoot ?? process.cwd(),
"plugins",
"app-intents",
);
config = withDangerousMod(config, [
"ios",
async (config) => {
const projectRoot = config.modRequest.projectRoot;
const platformRoot = config.modRequest.platformProjectRoot;
const projectName =
config.modRequest.projectName ??
IOSConfig.XcodeUtils.getProjectName(projectRoot);
const targetDir = path.join(platformRoot, projectName);
fs.mkdirSync(targetDir, { recursive: true });
for (const file of SWIFT_FILES) {
fs.copyFileSync(path.join(appIntentsSource, file), path.join(targetDir, file));
}
const appDelegatePath = path.join(targetDir, "AppDelegate.swift");
if (fs.existsSync(appDelegatePath)) {
let appDelegate = fs.readFileSync(appDelegatePath, "utf8");
if (!appDelegate.includes("import AppIntents")) {
appDelegate = appDelegate.replace(
"internal import Expo",
"import AppIntents\ninternal import Expo",
);
}
if (!appDelegate.includes("BeenVoiceShortcuts.updateAppShortcutParameters")) {
appDelegate = appDelegate.replace(
"return super.application(application, didFinishLaunchingWithOptions: launchOptions)",
`${SHORTCUT_REGISTRATION}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)`,
);
} else {
appDelegate = appDelegate.replace(
/if #available\(iOS 1[68]\.0, \*\) \{\s*Task \{\s*await BeenVoiceShortcuts\.updateAppShortcutParameters\(\)\s*\}\s*\}/g,
SHORTCUT_REGISTRATION,
);
}
if (!appDelegate.includes("applicationDidBecomeActive")) {
appDelegate = appDelegate.replace(
" // Linking API",
` public override func applicationDidBecomeActive(_ application: UIApplication) {
${SHORTCUT_REGISTRATION}
super.applicationDidBecomeActive(application)
}
// Linking API`,
);
}
fs.writeFileSync(appDelegatePath, appDelegate);
}
return config;
},
]);
return withXcodeProject(config, (config) => {
const project = config.modResults;
const projectRoot = config.modRequest.projectRoot;
const platformRoot = config.modRequest.platformProjectRoot;
const projectName =
config.modRequest.projectName ??
IOSConfig.XcodeUtils.getProjectName(projectRoot);
for (const file of SWIFT_FILES) {
const filepath = `${projectName}/${file}`;
const absolutePath = path.join(platformRoot, filepath);
if (!fs.existsSync(absolutePath)) continue;
const fileRef = project.pbxFileReferenceSection();
const alreadyLinked = Object.values(fileRef).some(
(entry) =>
entry &&
typeof entry === "object" &&
"path" in entry &&
(entry.path === file ||
entry.path === `${projectName}/${file}` ||
String(entry.path).endsWith(`/${file}`)),
);
if (!alreadyLinked) {
IOSConfig.XcodeUtils.addBuildSourceFileToGroup({
filepath,
groupName: projectName,
project,
verbose: true,
});
}
}
const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key];
if (
typeof buildConfig !== "object" ||
!buildConfig.buildSettings ||
buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER !== "com.beenvoice.app"
) {
continue;
}
buildConfig.buildSettings.EXTRACT_APP_INTENTS_METADATA = "YES";
}
return config;
});
}
module.exports = withAppIntents;
@@ -0,0 +1,64 @@
// @ts-check
const { withXcodeProject } = require("@expo/config-plugins");
const RELEASE_SIGN_KEY = '"CODE_SIGN_IDENTITY[sdk=iphoneos*]"';
const MAIN_BUNDLE_ID = "com.beenvoice.app";
const WIDGET_BUNDLE_ID = "com.beenvoice.app.ExpoWidgetsTarget";
/**
* Keep App Store archives on distribution signing. Automatic signing can select
* an Apple Development identity for the widget target when archiving from the CLI,
* so both release targets use their explicit App Store profiles instead.
*/
function configureReleaseSigning(project) {
const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key];
if (
!buildConfig ||
typeof buildConfig !== "object" ||
!buildConfig.buildSettings
) {
continue;
}
if (buildConfig.name !== "Release") {
continue;
}
const bundleId = String(
buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER ?? "",
).replaceAll('"', "");
if (bundleId !== MAIN_BUNDLE_ID && bundleId !== WIDGET_BUNDLE_ID) {
continue;
}
const profileName =
bundleId === WIDGET_BUNDLE_ID
? (process.env.IOS_WIDGET_APPSTORE_PROFILE_NAME ?? WIDGET_BUNDLE_ID)
: (process.env.IOS_MAIN_APPSTORE_PROFILE_NAME ?? MAIN_BUNDLE_ID);
buildConfig.buildSettings[RELEASE_SIGN_KEY] = '"Apple Distribution"';
buildConfig.buildSettings.CODE_SIGN_STYLE = "Manual";
buildConfig.buildSettings.PROVISIONING_PROFILE_SPECIFIER = `"${profileName}"`;
if (process.env.APPLE_TEAM_ID) {
buildConfig.buildSettings.DEVELOPMENT_TEAM = process.env.APPLE_TEAM_ID;
}
}
return project;
}
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withAppStoreSigning(config) {
return withXcodeProject(config, (config) => {
configureReleaseSigning(config.modResults);
return config;
});
}
module.exports = withAppStoreSigning;
module.exports.configureReleaseSigning = configureReleaseSigning;
+104
View File
@@ -0,0 +1,104 @@
// @ts-check
const { createRunOncePlugin, withDangerousMod } = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
const pkg = require("expo-mlkit-ocr/package.json");
const DISABLE_LINE = "ENV['EXPO_MLKIT_OCR_DISABLE_MLKIT'] = '1'";
const MARKER = "expo-mlkit-ocr: iOS Simulator MLKit handling";
function resolveIosEngine(props = {}) {
const raw = props.iosEngine ?? "auto";
return raw === "auto" || raw === "mlkit" || raw === "vision" ? raw : "auto";
}
function stripMarkedBlock(podfile) {
const lines = podfile.split(/\r?\n/);
const next = [];
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
if (!line.includes(MARKER)) {
next.push(line);
continue;
}
const markerIndent = (line.match(/^\s*/) ?? [""])[0].length;
while (i + 1 < lines.length) {
i += 1;
const candidate = lines[i];
const candidateIndent = (candidate.match(/^\s*/) ?? [""])[0].length;
if (candidate.trim() === "end" && candidateIndent <= markerIndent) {
break;
}
}
}
return next.join("\n");
}
function stripOrphanedMlkitArchPatch(podfile) {
return podfile
.replace(
/\n\s*end\s*\n\s*\n\s*installer\.aggregate_targets\.each do \|aggregate_target\|[\s\S]*?aggregate_target\.user_project\.save\s*\n\s*end\s*\n/g,
"\n",
)
.replace(
/\n\s*if ENV\['EXPO_MLKIT_OCR_DISABLE_MLKIT'\] == '1'\s*\n\s*installer\.pods_project\.targets\.each do \|target\|[\s\S]*?\n\s*end\s*\n/g,
"\n",
);
}
function insertDisableLine(podfile) {
const lines = podfile
.split(/\r?\n/)
.filter((line) => line.trim() !== DISABLE_LINE);
let insertAt = 0;
while (insertAt < lines.length && lines[insertAt].trim().startsWith("#")) {
insertAt += 1;
}
lines.splice(insertAt, 0, DISABLE_LINE);
return lines.join("\n");
}
/** @type {import('@expo/config-plugins').ConfigPlugin<{ iosEngine?: "auto" | "mlkit" | "vision", disableMlkitOnSimulator?: boolean }>} */
function withExpoMlkitOcrEnv(config, props = {}) {
return withDangerousMod(config, [
"ios",
async (config) => {
const podfilePath = path.join(config.modRequest.platformProjectRoot, "Podfile");
if (!fs.existsSync(podfilePath)) {
return config;
}
const iosEngine = resolveIosEngine(props);
const shouldDisableMlkit =
iosEngine !== "mlkit" || props.disableMlkitOnSimulator === true;
let podfile = fs.readFileSync(podfilePath, "utf8");
podfile = stripMarkedBlock(podfile);
podfile = stripOrphanedMlkitArchPatch(podfile);
if (shouldDisableMlkit) {
podfile = insertDisableLine(podfile);
} else {
podfile = podfile
.split(/\r?\n/)
.filter((line) => line.trim() !== DISABLE_LINE)
.join("\n");
}
fs.writeFileSync(podfilePath, podfile.endsWith("\n") ? podfile : `${podfile}\n`);
return config;
},
]);
}
module.exports = createRunOncePlugin(
withExpoMlkitOcrEnv,
"beenvoice-expo-mlkit-ocr-env",
pkg.version,
);
@@ -0,0 +1,82 @@
// @ts-check
const fs = require("fs");
const path = require("path");
const { withDangerousMod } = require("@expo/config-plugins");
const widgetsPath = path.join("node_modules", "expo-widgets", "ios", "Widgets");
const liveActivityBannerSource = `import SwiftUI
import WidgetKit
import ActivityKit
@available(iOS 18.0, *)
struct LiveActivityBanner: View {
@Environment(\\.activityFamily) var activityFamily
var context: ActivityViewContext<LiveActivityAttributes>
var nodes: [String: Any]?
var body: some View {
if let nodes, let node = nodes["banner"] as? [String: Any] {
WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
} else {
EmptyView()
}
}
}
`;
function patchWidgetLiveActivitySwift(source) {
const target = ` if #available(iOS 18.0, *) {
LiveActivityBanner(context: context, nodes: nodes)
} else if let node = nodes["banner"] as? [String: Any] {
WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
} else {
EmptyView()
}`;
const framedTarget = ` if #available(iOS 18.0, *) {
LiveActivityBanner(context: context, nodes: nodes)
} else if let node = nodes["banner"] as? [String: Any] {
WidgetsDynamicView(name: context.activityID, kind: .liveActivity, node: node)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
EmptyView()
}`;
const hardcodedBanner = ` if #available(iOS 18.0, *) {
LiveActivityBanner(context: context, nodes: nodes)
} else {
TimeClockNativeActivityBanner(context: context)
}`;
if (source.includes(target)) {
return source;
}
return source.replace(framedTarget, target).replace(hardcodedBanner, target);
}
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withLiveActivityBannerFrame(config) {
return withDangerousMod(config, [
"ios",
async (config) => {
const sourceDir = path.join(config.modRequest.projectRoot, widgetsPath);
const bannerPath = path.join(sourceDir, "LiveActivityBanner.swift");
const activityPath = path.join(sourceDir, "WidgetLiveActivity.swift");
if (!fs.existsSync(bannerPath) || !fs.existsSync(activityPath)) {
throw new Error(`Could not find expo-widgets live activity sources in ${sourceDir}`);
}
fs.writeFileSync(bannerPath, liveActivityBannerSource);
const activitySource = fs.readFileSync(activityPath, "utf8");
const patchedActivitySource = patchWidgetLiveActivitySwift(activitySource);
if (patchedActivitySource !== activitySource) {
fs.writeFileSync(activityPath, patchedActivitySource);
}
return config;
},
]);
}
module.exports = withLiveActivityBannerFrame;
@@ -0,0 +1,256 @@
// @ts-check
const fs = require("fs");
const path = require("path");
const { withDangerousMod } = require("@expo/config-plugins");
const dynamicViewPath = path.join(
"node_modules",
"expo-widgets",
"ios",
"Widgets",
"DynamicView.swift",
);
// expo-widgets rebuilds every WidgetsDynamicView child with a brand-new random UUID
// on every single render (see the original "Hack to satisfy ExpoSwiftUI.AnyChild"
// comment), so the `ForEach(props.children, id: \.id)` that HStack/VStack use to
// render their children sees a totally different identity set on each re-render.
// SwiftUI then treats every child as removed-and-reinserted instead of updated in
// place, which can make children silently fail to render whenever the parent's
// layout is recomputed (e.g. after a `.frame(maxWidth:)` change). This patch gives
// each child a stable identity derived from its structural position, cached across
// renders, so ForEach can correctly diff instead of thrashing.
const patchedSource = `import SwiftUI
import ExpoModulesCore
import ExpoUI
// TODO(@jakex7): Hack to satisfy ExpoSwiftUI.AnyChild with random UUID value
class NodeIdentityWrapper {
let id: UUID
init(id: UUID) {
self.id = id
}
}
// Reuses the same NodeIdentityWrapper for a given structural key across re-renders,
// so ForEach(id: \\.id) sees a stable identity instead of a fresh UUID every render.
private final class NodeIdentityCache {
static let shared = NodeIdentityCache()
private var wrappers: [String: NodeIdentityWrapper] = [:]
func wrapper(for key: String) -> NodeIdentityWrapper {
if let existing = wrappers[key] {
return existing
}
let created = NodeIdentityWrapper(id: UUID())
wrappers[key] = created
return created
}
}
extension ObjectIdentifier: @retroactive Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(String(describing: self))
}
}
public struct WidgetsDynamicView: View, ExpoSwiftUI.AnyChild {
let node: [String: Any]
let name: String
let kind: WidgetsKind
let entryIndex: Int?
let environmentString: String?
let childKey: String?
private var uuid: NodeIdentityWrapper {
guard let childKey else {
return NodeIdentityWrapper(id: UUID())
}
return NodeIdentityCache.shared.wrapper(for: childKey)
}
public var id: ObjectIdentifier {
ObjectIdentifier(uuid)
}
public init(name: String, kind: WidgetsKind, node: [String: Any]) {
self.name = name
self.kind = kind
self.node = node
self.entryIndex = nil
self.environmentString = nil
self.childKey = node["type"] as? String
}
public init(name: String, kind: WidgetsKind, node: [String: Any], entryIndex: Int?, environmentString: String?) {
self.name = name
self.kind = kind
self.node = node
self.entryIndex = entryIndex
self.environmentString = environmentString
self.childKey = node["type"] as? String
}
private init(name: String, kind: WidgetsKind, node: [String: Any], entryIndex: Int?, environmentString: String?, childKey: String) {
self.name = name
self.kind = kind
self.node = node
self.entryIndex = entryIndex
self.environmentString = environmentString
self.childKey = childKey
}
@ViewBuilder
public var body: some View {
switch node["type"] as? String {
case "TextView":
render(TextView.self, TextViewProps.self, updateProps: updateChildren)
case "HStackView":
render(HStackView.self, HStackViewProps.self, updateProps: updateChildren)
case "VStackView":
render(VStackView.self, VStackViewProps.self, updateProps: updateChildren)
case "ZStackView":
render(ZStackView.self, ZStackViewProps.self, updateProps: updateChildren)
case "RectangleView":
render(RectangleView.self, RectangleViewProps.self)
case "RoundedRectangleView":
render(RoundedRectangleView.self, RoundedRectangleViewProps.self)
case "CapsuleView":
render(CapsuleView.self, CapsuleViewProps.self)
case "CircleView":
render(CircleView.self, CircleViewProps.self)
case "ImageView":
render(ImageView.self, ImageViewProps.self)
case "AccessoryWidgetBackgroundView":
render(AccessoryWidgetBackgroundView.self, AccessoryWidgetBackgroundProps.self)
case "DividerView":
render(DividerView.self, DividerProps.self)
case "EllipseView":
render(EllipseView.self, EllipseViewProps.self)
case "LabelView":
render(LabelView.self, LabelViewProps.self)
case "ProgressView":
render(ProgressView.self, ProgressViewProps.self)
case "SpacerView":
render(SpacerView.self, SpacerViewProps.self)
case "UnevenRoundedRectangleView":
render(UnevenRoundedRectangleView.self, UnevenRoundedRectangleViewProps.self)
case "GaugeView":
render(GaugeView.self, GaugeProps.self)
case "ChartView":
render(ChartView.self, ChartProps.self)
case "Button":
if #available(iOS 17.0, *) {
switch kind {
case .widget:
render(WidgetButtonView.self, ButtonProps.self) { buttonProps in
try updateChildren(buttonProps)
buttonProps.source = name
buttonProps.entryIndex = entryIndex
buttonProps.environmentString = environmentString
}
case .liveActivity:
render(LiveActivityButtonView.self, ButtonProps.self) { buttonProps in
try updateChildren(buttonProps)
buttonProps.source = name
}
}
} else {
render(ExpoUI.Button.self, ExpoUI.ButtonProps.self, updateProps: updateChildren)
}
case "react.fragment":
render(FragmentView.self, FragmentProps.self, updateProps: updateChildren)
case "LinkView":
render(LinkView.self, LinkViewProps.self, updateProps: updateChildren)
#if DEBUG
case "RedBoxView":
render(RedBoxView.self, RedBoxViewProps.self) { redBoxProps in
redBoxProps.source = name
redBoxProps.kind = kind
}
default:
ZStack {
Color.red.opacity(0.5)
Text("Unable to get the view for: \\(node["type"] as? String ?? "undefined")")
}
#else
default:
EmptyView()
#endif
}
}
// MARK: - Render Method
@ViewBuilder
private func render<P, V>(_ viewType: V.Type, _ propsType: P.Type, updateProps: ((_ initialProps: P) throws -> Void)? = nil) -> some View
where P: UIBaseViewProps, V: ExpoSwiftUI.View, V.Props == P {
// immediately invoked closure {}() here because we can't use 'do-catch' inside @ViewBuilder
{
do {
if let rawProps = node["props"] as? [String: Any] {
let props = try propsType.init(rawProps: rawProps, context: WidgetsContext.shared.context)
try updateProps?(props)
return AnyView(UIBaseView<P, V>(props: props).transition(.identity))
}
return AnyView(EmptyView())
} catch {
return AnyView(EmptyView())
}
}()
}
// MARK: - Function that sets children as DynamicView
private func updateChildren<P>(_ initialProps: P) throws
where P: UIBaseViewProps {
let baseKey = childKey ?? (node["type"] as? String ?? "root")
if let props = node["props"] as? [String: Any] {
if let children = props["children"] as? [Any] {
let validChildren = children.compactMap { $0 as? [String: Any] }
initialProps.children = validChildren.enumerated().map { index, childNode in
let childType = childNode["type"] as? String ?? "unknown"
return WidgetsDynamicView(
name: name,
kind: kind,
node: childNode,
entryIndex: entryIndex,
environmentString: environmentString,
childKey: "\\(baseKey).\\(index).\\(childType)"
)
}
} else if let child = props["children"] as? [String: Any] {
let childType = child["type"] as? String ?? "unknown"
initialProps.children = [WidgetsDynamicView(
name: name,
kind: kind,
node: child,
entryIndex: entryIndex,
environmentString: environmentString,
childKey: "\\(baseKey).0.\\(childType)"
)]
}
}
}
}
`;
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withStableWidgetsChildIdentity(config) {
return withDangerousMod(config, [
"ios",
async (config) => {
const filePath = path.join(config.modRequest.projectRoot, dynamicViewPath);
if (!fs.existsSync(filePath)) {
throw new Error(`Could not find expo-widgets DynamicView.swift at ${filePath}`);
}
fs.writeFileSync(filePath, patchedSource);
return config;
},
]);
}
module.exports = withStableWidgetsChildIdentity;
@@ -0,0 +1,34 @@
// @ts-check
const { withXcodeProject } = require("@expo/config-plugins");
/**
* expo-widgets seeds ExpoWidgetsTarget with hardcoded MARKETING_VERSION/CURRENT_PROJECT_VERSION
* and does not update them on later prebuilds. App Store Connect warns when the extension
* does not match the containing app.
*/
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withSyncWidgetVersions(config) {
return withXcodeProject(config, (config) => {
const marketingVersion = config.version ?? "1.0.0";
const buildNumber = config.ios?.buildNumber ?? "1";
const project = config.modResults;
const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key];
if (!buildConfig?.buildSettings) continue;
const bundleId = buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER;
if (!bundleId || !String(bundleId).includes("ExpoWidgetsTarget")) {
continue;
}
buildConfig.buildSettings.MARKETING_VERSION = marketingVersion;
buildConfig.buildSettings.CURRENT_PROJECT_VERSION = buildNumber;
}
return config;
});
}
module.exports = withSyncWidgetVersions;