@use-voltra/ios-client 2.3.2 → 2.4.0
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.
- package/build/commonjs/dynamic-widget/api.js +28 -1
- package/build/commonjs/dynamic-widget/api.js.map +1 -1
- package/build/commonjs/index.js +7 -0
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/native/NativeVoltra.js.map +1 -1
- package/build/module/dynamic-widget/api.js +26 -0
- package/build/module/dynamic-widget/api.js.map +1 -1
- package/build/module/index.js +1 -1
- package/build/module/index.js.map +1 -1
- package/build/module/native/NativeVoltra.js.map +1 -1
- package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/commonjs/index.d.ts +1 -1
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
- package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
- package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
- package/build/typescript/module/index.d.ts +1 -1
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
- package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
- package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
- package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
- package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
- package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/cjs/validation.js +1 -0
- package/expo-plugin/build/cjs/validation.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
- package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
- package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
- package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
- package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
- package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
- package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
- package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
- package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
- package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
- package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
- package/expo-plugin/build/esm/validation.js +2 -1
- package/expo-plugin/build/esm/validation.js.map +1 -1
- package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
- package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
- package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
- package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
- package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
- package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
- package/expo-plugin/build/types/types.d.ts +13 -4
- package/expo-plugin/build/types/types.d.ts.map +1 -1
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
- package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
- package/expo-plugin/build/types/validation.d.ts.map +1 -1
- package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
- package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
- package/expo-plugin/src/ios-widget/files/index.ts +14 -1
- package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
- package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
- package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
- package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
- package/expo-plugin/src/types.ts +13 -3
- package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
- package/expo-plugin/src/validation.ts +2 -0
- package/ios/Package.swift +8 -1
- package/ios/VoltraWidget.podspec +1 -1
- package/ios/app/NativeVoltra.mm +15 -2
- package/ios/app/VoltraModule.swift +11 -0
- package/ios/app/VoltraModuleImpl.swift +14 -0
- package/ios/app/VoltraViewComponentView.mm +2 -2
- package/ios/app/VoltraViewRoot.swift +6 -12
- package/ios/shared/ShortNames.swift +1 -0
- package/ios/shared/VoltraConstants.swift +4 -0
- package/ios/shared/VoltraElement.swift +43 -2
- package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
- package/ios/shared/VoltraModifierDescriptor.swift +33 -0
- package/ios/shared/VoltraWidgetDefaults.swift +24 -0
- package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
- package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
- package/ios/target/VoltraHomeWidget.swift +10 -15
- package/ios/target/VoltraWidget.swift +26 -18
- package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
- package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
- package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
- package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
- package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
- package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
- package/ios/ui/Style/TextStyle.swift +4 -2
- package/ios/ui/Style/View+applyStyle.swift +1 -1
- package/ios/ui/Views/VoltraText.swift +4 -8
- package/package.json +4 -4
- package/src/dynamic-widget/api.ts +28 -0
- package/src/index.ts +6 -1
- package/src/native/NativeVoltra.ts +2 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
-
import type { DynamicLiveActivityEntryConfig, DynamicWidgetEntryConfig, WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin';
|
|
2
|
+
import type { DynamicLiveActivityEntryConfig, DynamicWidgetEntryConfig, WidgetConfigurationOption, WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin';
|
|
3
3
|
/**
|
|
4
4
|
* Supported iOS Home Screen widget size families.
|
|
5
5
|
*/
|
|
@@ -10,10 +10,19 @@ export type IOSWidgetFamily = 'systemSmall' | 'systemMedium' | 'systemLarge' | '
|
|
|
10
10
|
export interface AppIntentParameter {
|
|
11
11
|
/** Swift property name + the key under `env.configuration`. */
|
|
12
12
|
name: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Label shown in the widget configuration sheet: a plain string, or a locale map
|
|
15
|
+
* (`{ "en": "Units", "pl": "Jednostki" }`) written to the extension's `Localizable.strings`.
|
|
16
|
+
*/
|
|
17
|
+
title: WidgetLabel;
|
|
18
|
+
/** Default value used before the user configures the widget (the "from code" default). Must be
|
|
19
|
+
* one of `options[].value` when `options` is set. */
|
|
16
20
|
default?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Fixed values the user picks from. The sheet shows a picker with each option's (localisable)
|
|
23
|
+
* `title` instead of a free-text field; `env.configuration` receives the option's `value`.
|
|
24
|
+
*/
|
|
25
|
+
options?: WidgetConfigurationOption[];
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* AppIntent configuration for a user-configurable widget (iOS 17+).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,KAAK,EACV,8BAA8B,EAC9B,wBAAwB,EACxB,sBAAsB,EACtB,WAAW,EACZ,MAAM,yBAAyB,CAAA;AAEhC;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,aAAa,GACb,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,mBAAmB,GACnB,sBAAsB,GACtB,iBAAiB,CAAA;AAErB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { IOSWidgetExtensionFiles } from '../types';
|
|
2
2
|
/** Table name matches basename without extension; co-located per locale in *.lproj */
|
|
3
3
|
export declare const VOLTRA_WIDGET_STRINGS_BASENAME = "VoltraWidgets.strings";
|
|
4
|
+
/** Default table: Edit Widget sheet strings, and the file that makes each `.lproj` a real resource. */
|
|
5
|
+
export declare const VOLTRA_LOCALIZABLE_STRINGS_BASENAME = "Localizable.strings";
|
|
4
6
|
/**
|
|
5
7
|
* Scans the widget extension target directory and returns categorized file lists.
|
|
6
8
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileDiscovery.d.ts","sourceRoot":"","sources":["../../../src/utils/fileDiscovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAGvD,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,0BAA0B,CAAA;AAErE;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,uBAAuB,
|
|
1
|
+
{"version":3,"file":"fileDiscovery.d.ts","sourceRoot":"","sources":["../../../src/utils/fileDiscovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAGvD,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,0BAA0B,CAAA;AAErE,uGAAuG;AACvG,eAAO,MAAM,mCAAmC,wBAAwB,CAAA;AAExE;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,uBAAuB,CA6E1G"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,eAAe,EAAmB,MAAM,SAAS,CAAA;AAanH,wBAAgB,oCAAoC,CAClD,YAAY,EAAE,4BAA4B,EAC1C,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAaN;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CA8B3F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,oBAAoB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAwEpG"}
|
|
@@ -42,6 +42,10 @@ function buildPlaceholderEnv(voltraVersion: string): Record<string, unknown> {
|
|
|
42
42
|
widgetFamily: 'systemMedium',
|
|
43
43
|
colorScheme: 'light',
|
|
44
44
|
locale: 'en-US',
|
|
45
|
+
preferredLanguages: ['en-US'],
|
|
46
|
+
layoutDirection: 'ltr',
|
|
47
|
+
hourCycle: 'h12',
|
|
48
|
+
timeZone: 'UTC',
|
|
45
49
|
widgetRenderingMode: 'fullColor',
|
|
46
50
|
showsWidgetContainerBackground: true,
|
|
47
51
|
configuration: undefined,
|
|
@@ -26,7 +26,15 @@ export function generateDynamicLiveActivityTypesSwift(liveActivities: IOSDynamic
|
|
|
26
26
|
import ActivityKit
|
|
27
27
|
import Foundation
|
|
28
28
|
|
|
29
|
+
// This file compiles in both the app target (pod Voltra, module
|
|
30
|
+
// VoltraRuntime) and the widget extension target (pod VoltraWidget,
|
|
31
|
+
// module VoltraWidgetRuntime). The two pods must keep distinct module
|
|
32
|
+
// names so their build products never collide.
|
|
33
|
+
#if canImport(VoltraWidgetRuntime)
|
|
34
|
+
import VoltraWidgetRuntime
|
|
35
|
+
#else
|
|
29
36
|
import VoltraRuntime
|
|
37
|
+
#endif
|
|
30
38
|
|
|
31
39
|
@objc(VoltraGeneratedDynamicLiveActivityRegistration)
|
|
32
40
|
public final class VoltraGeneratedDynamicLiveActivityRegistration: NSObject {
|
|
@@ -97,7 +105,7 @@ export function generateDynamicLiveActivitiesSwift(liveActivities: IOSDynamicLiv
|
|
|
97
105
|
import ActivityKit
|
|
98
106
|
import SwiftUI
|
|
99
107
|
import WidgetKit
|
|
100
|
-
import
|
|
108
|
+
import VoltraWidgetRuntime
|
|
101
109
|
|
|
102
110
|
`
|
|
103
111
|
return [header.trim(), configurations.trim()].filter(Boolean).join('\n\n')
|
|
@@ -6,6 +6,11 @@ import type { IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
|
|
|
6
6
|
import { generateAssets } from './assets'
|
|
7
7
|
import { generateEntitlements } from './entitlements'
|
|
8
8
|
import { generateInfoPlist } from './infoPlist'
|
|
9
|
+
import {
|
|
10
|
+
resolveDevelopmentRegion,
|
|
11
|
+
resolveExtensionLocalizations,
|
|
12
|
+
syncExtensionLocalizableStrings,
|
|
13
|
+
} from './localization'
|
|
9
14
|
import { generateIOSDynamicLiveActivitiesManifest, generateIOSDynamicWidgetsManifest } from './manifest'
|
|
10
15
|
import { generateSwiftFiles } from './swift'
|
|
11
16
|
|
|
@@ -29,6 +34,7 @@ export interface GenerateWidgetExtensionFilesProps {
|
|
|
29
34
|
* - VoltraWidgetBundle.swift (widget bundle definition)
|
|
30
35
|
* - VoltraWidgetInitialStates.swift (pre-rendered widget states)
|
|
31
36
|
* - {targetName}.entitlements (entitlements file)
|
|
37
|
+
* - <locale>.lproj/Localizable.strings (declared languages and Edit Widget sheet strings)
|
|
32
38
|
*
|
|
33
39
|
* This should run before configureXcodeProject so the files exist when Xcode project is configured.
|
|
34
40
|
*/
|
|
@@ -51,8 +57,11 @@ export const generateWidgetExtensionFiles: ConfigPlugin<GenerateWidgetExtensionF
|
|
|
51
57
|
fs.mkdirSync(targetPath, { recursive: true })
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
// Languages the extension declares, mirrored from the app (ADR 0009 §4)
|
|
61
|
+
const localizations = resolveExtensionLocalizations(config, widgets)
|
|
62
|
+
|
|
54
63
|
// Generate Info.plist
|
|
55
|
-
generateInfoPlist(targetPath, targetName, version, buildNumber, voltraVersion)
|
|
64
|
+
generateInfoPlist(targetPath, targetName, version, buildNumber, voltraVersion, localizations)
|
|
56
65
|
|
|
57
66
|
// Generate Assets.xcassets and copy user images
|
|
58
67
|
generateAssets({ targetPath })
|
|
@@ -65,6 +74,10 @@ export const generateWidgetExtensionFiles: ConfigPlugin<GenerateWidgetExtensionF
|
|
|
65
74
|
liveActivities,
|
|
66
75
|
})
|
|
67
76
|
|
|
77
|
+
// <locale>.lproj/Localizable.strings: Edit Widget sheet strings, and one real resource per
|
|
78
|
+
// declared language. Runs after the gallery strings, which share the .lproj folders.
|
|
79
|
+
syncExtensionLocalizableStrings(targetPath, localizations, widgets, resolveDevelopmentRegion(config))
|
|
80
|
+
|
|
68
81
|
// Write the iOS-owned Dynamic Widgets manifest for Metro to consume later.
|
|
69
82
|
generateIOSDynamicWidgetsManifest({
|
|
70
83
|
projectRoot,
|
|
@@ -3,6 +3,10 @@ import * as path from 'path'
|
|
|
3
3
|
|
|
4
4
|
import { logger } from '@use-voltra/expo-plugin'
|
|
5
5
|
|
|
6
|
+
function escapeXml(value: string): string {
|
|
7
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Generates the Info.plist content for a WidgetKit extension.
|
|
8
12
|
*
|
|
@@ -13,8 +17,17 @@ function generateInfoPlistContent(
|
|
|
13
17
|
targetName: string,
|
|
14
18
|
version: string,
|
|
15
19
|
buildNumber: string,
|
|
16
|
-
voltraVersion: string
|
|
20
|
+
voltraVersion: string,
|
|
21
|
+
localizations: string[]
|
|
17
22
|
): string {
|
|
23
|
+
// CFBundleLocalizations mirrors the app's languages into the extension (ADR 0009 §4), so
|
|
24
|
+
// Locale.current and SwiftUI's \.locale there resolve the way they do in the app.
|
|
25
|
+
const localizationsEntry =
|
|
26
|
+
localizations.length > 0
|
|
27
|
+
? `\t<key>CFBundleLocalizations</key>\n\t<array>\n${localizations
|
|
28
|
+
.map((locale) => `\t\t<string>${escapeXml(locale)}</string>\n`)
|
|
29
|
+
.join('')}\t</array>\n`
|
|
30
|
+
: ''
|
|
18
31
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
19
32
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
20
33
|
<plist version="1.0">
|
|
@@ -27,7 +40,7 @@ function generateInfoPlistContent(
|
|
|
27
40
|
<string>$(EXECUTABLE_NAME)</string>
|
|
28
41
|
<key>CFBundleIdentifier</key>
|
|
29
42
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
30
|
-
<key>CFBundleInfoDictionaryVersion</key>
|
|
43
|
+
${localizationsEntry} <key>CFBundleInfoDictionaryVersion</key>
|
|
31
44
|
<string>6.0</string>
|
|
32
45
|
<key>CFBundleName</key>
|
|
33
46
|
<string>$(PRODUCT_NAME)</string>
|
|
@@ -57,15 +70,21 @@ function generateInfoPlistContent(
|
|
|
57
70
|
* @param version - The app version (CFBundleShortVersionString)
|
|
58
71
|
* @param buildNumber - The build number (CFBundleVersion)
|
|
59
72
|
* @param voltraVersion - The installed @use-voltra/ios-client package version
|
|
73
|
+
* @param localizations - Languages the extension declares (`CFBundleLocalizations`); see
|
|
74
|
+
* `resolveExtensionLocalizations`
|
|
60
75
|
*/
|
|
61
76
|
export function generateInfoPlist(
|
|
62
77
|
targetPath: string,
|
|
63
78
|
targetName: string,
|
|
64
79
|
version: string,
|
|
65
80
|
buildNumber: string,
|
|
66
|
-
voltraVersion: string
|
|
81
|
+
voltraVersion: string,
|
|
82
|
+
localizations: string[] = []
|
|
67
83
|
): void {
|
|
68
84
|
const infoPlistPath = path.join(targetPath, 'Info.plist')
|
|
69
|
-
fs.writeFileSync(
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
infoPlistPath,
|
|
87
|
+
generateInfoPlistContent(targetName, version, buildNumber, voltraVersion, localizations)
|
|
88
|
+
)
|
|
70
89
|
logger.info('Generated Info.plist')
|
|
71
90
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
collectLabelLocaleKeys,
|
|
6
|
+
collectWidgetConfigurationStrings,
|
|
7
|
+
isWidgetLocalizedMap,
|
|
8
|
+
normalizeLocaleTag,
|
|
9
|
+
pickLocalizedValue,
|
|
10
|
+
widgetLabelEnglish,
|
|
11
|
+
type WidgetLabel,
|
|
12
|
+
} from '@use-voltra/expo-plugin'
|
|
13
|
+
|
|
14
|
+
import type { IOSWidgetConfig } from '../../types'
|
|
15
|
+
import { VOLTRA_LOCALIZABLE_STRINGS_BASENAME } from '../../utils/fileDiscovery'
|
|
16
|
+
import { escapeForSwiftStringLiteral } from './swift-utils'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The parts of the Expo config that say which languages the app supports: the `locales` map
|
|
20
|
+
* (`{ "pl": "./languages/pl.json" }`) and an explicit `ios.infoPlist.CFBundleLocalizations`.
|
|
21
|
+
*/
|
|
22
|
+
export interface AppLocalizationSource {
|
|
23
|
+
locales?: Record<string, unknown> | null
|
|
24
|
+
ios?: { infoPlist?: Record<string, unknown> | null } | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function widgetLabels(widget: IOSWidgetConfig): Array<WidgetLabel | undefined> {
|
|
28
|
+
return [
|
|
29
|
+
widget.displayName,
|
|
30
|
+
widget.description,
|
|
31
|
+
...(widget.appIntent?.parameters ?? []).flatMap((parameter) => [
|
|
32
|
+
parameter.title,
|
|
33
|
+
...(parameter.options ?? []).map((option) => option.title),
|
|
34
|
+
]),
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LOCALE_TAG_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The app's development language: `ios.infoPlist.CFBundleDevelopmentRegion` when it is a literal
|
|
42
|
+
* locale tag, otherwise `en` (Expo's default, and what `$(DEVELOPMENT_LANGUAGE)` is in practice).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveDevelopmentRegion(config: AppLocalizationSource | undefined): string {
|
|
45
|
+
const region = config?.ios?.infoPlist?.CFBundleDevelopmentRegion
|
|
46
|
+
return typeof region === 'string' && LOCALE_TAG_PATTERN.test(region.trim()) ? region.trim() : 'en'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The languages the widget extension declares (ADR 0009 §4).
|
|
51
|
+
*
|
|
52
|
+
* iOS resolves `Locale.current` and SwiftUI's `\.locale` in the extension against the extension
|
|
53
|
+
* bundle's own localisations, so an extension that declares none reports `en_PL` to a Polish user
|
|
54
|
+
* of a Polish app. Mirroring the app's languages makes the extension resolve the way the app does.
|
|
55
|
+
*
|
|
56
|
+
* Sources: the Expo `locales` keys, `ios.infoPlist.CFBundleLocalizations`, and every locale used
|
|
57
|
+
* by a widget's locale maps. The app's development language (see `resolveDevelopmentRegion`) is
|
|
58
|
+
* added whenever anything is declared: it is the language iOS falls back to when none of the user's
|
|
59
|
+
* languages match, so its `.lproj` must exist and carry every string.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveExtensionLocalizations(
|
|
62
|
+
config: AppLocalizationSource | undefined,
|
|
63
|
+
widgets: IOSWidgetConfig[] | undefined
|
|
64
|
+
): string[] {
|
|
65
|
+
const candidates: string[] = []
|
|
66
|
+
|
|
67
|
+
for (const locale of Object.keys(config?.locales ?? {})) {
|
|
68
|
+
candidates.push(locale)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const infoPlistLocalizations = config?.ios?.infoPlist?.CFBundleLocalizations
|
|
72
|
+
if (Array.isArray(infoPlistLocalizations)) {
|
|
73
|
+
for (const locale of infoPlistLocalizations) {
|
|
74
|
+
if (typeof locale === 'string') {
|
|
75
|
+
candidates.push(locale)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
candidates.push(...collectLabelLocaleKeys((widgets ?? []).flatMap(widgetLabels)))
|
|
81
|
+
|
|
82
|
+
const byNormalized = new Map<string, string>()
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
const trimmed = candidate.trim()
|
|
85
|
+
if (!trimmed || trimmed === '__default' || !LOCALE_TAG_PATTERN.test(trimmed)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const normalized = normalizeLocaleTag(trimmed)
|
|
89
|
+
if (!byNormalized.has(normalized)) {
|
|
90
|
+
byNormalized.set(normalized, trimmed)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const developmentRegion = resolveDevelopmentRegion(config)
|
|
95
|
+
const developmentLanguage = normalizeLocaleTag(developmentRegion).split('-')[0]
|
|
96
|
+
if (byNormalized.size > 0 && ![...byNormalized.keys()].some((key) => key.split('-')[0] === developmentLanguage)) {
|
|
97
|
+
byNormalized.set(normalizeLocaleTag(developmentRegion), developmentRegion)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [...byNormalized.values()].sort((a, b) => a.localeCompare(b))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The Edit Widget sheet strings for one locale: every locale-mapped title, resolved for `locale`,
|
|
105
|
+
* then the development language, then the usual fallback (English, `__default`, first), so every
|
|
106
|
+
* key resolves in every `.lproj` and none shows verbatim. Plain-string titles are compiled into the
|
|
107
|
+
* Swift source as literals and need no entry.
|
|
108
|
+
*/
|
|
109
|
+
export function collectSheetStringsForLocale(
|
|
110
|
+
widgets: IOSWidgetConfig[] | undefined,
|
|
111
|
+
locale: string,
|
|
112
|
+
developmentRegion = 'en'
|
|
113
|
+
): Record<string, string> {
|
|
114
|
+
const entries: Record<string, string> = {}
|
|
115
|
+
for (const widget of widgets ?? []) {
|
|
116
|
+
for (const { key, label } of collectWidgetConfigurationStrings(widget)) {
|
|
117
|
+
if (!isWidgetLocalizedMap(label)) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
entries[key] = pickLocalizedValue(label, [locale, developmentRegion]) ?? widgetLabelEnglish(label)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return entries
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatLocalizableStrings(entries: Record<string, string>): string {
|
|
127
|
+
const lines = Object.keys(entries)
|
|
128
|
+
.sort()
|
|
129
|
+
.map((key) => `"${escapeForSwiftStringLiteral(key)}" = "${escapeForSwiftStringLiteral(entries[key]!)}";`)
|
|
130
|
+
return `/* Voltra widget extension strings (auto-generated) */\n${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Writes `<locale>.lproj/Localizable.strings` for every declared language. The file carries the
|
|
135
|
+
* Edit Widget sheet strings (ADR 0009 §6) and, even when empty, makes the `.lproj` folder a real
|
|
136
|
+
* resource of the extension target so iOS sees the language as supported.
|
|
137
|
+
*
|
|
138
|
+
* The extension is generated and owned by Voltra, so every `Localizable.strings` in it is ours to
|
|
139
|
+
* rewrite; stale ones from a removed language are deleted.
|
|
140
|
+
*/
|
|
141
|
+
export function syncExtensionLocalizableStrings(
|
|
142
|
+
targetPath: string,
|
|
143
|
+
locales: string[],
|
|
144
|
+
widgets: IOSWidgetConfig[] | undefined,
|
|
145
|
+
developmentRegion = 'en'
|
|
146
|
+
): void {
|
|
147
|
+
if (fs.existsSync(targetPath)) {
|
|
148
|
+
for (const entry of fs.readdirSync(targetPath)) {
|
|
149
|
+
if (!entry.endsWith('.lproj')) {
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
const dirPath = path.join(targetPath, entry)
|
|
153
|
+
const stringsPath = path.join(dirPath, VOLTRA_LOCALIZABLE_STRINGS_BASENAME)
|
|
154
|
+
if (fs.existsSync(stringsPath)) {
|
|
155
|
+
fs.unlinkSync(stringsPath)
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (fs.readdirSync(dirPath).length === 0) {
|
|
159
|
+
fs.rmdirSync(dirPath)
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
/* ignore */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const locale of locales) {
|
|
168
|
+
const lproj = path.join(targetPath, `${locale}.lproj`)
|
|
169
|
+
fs.mkdirSync(lproj, { recursive: true })
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(lproj, VOLTRA_LOCALIZABLE_STRINGS_BASENAME),
|
|
172
|
+
formatLocalizableStrings(collectSheetStringsForLocale(widgets, locale, developmentRegion)),
|
|
173
|
+
'utf8'
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
logger,
|
|
8
8
|
prerenderWidgetState,
|
|
9
9
|
widgetLabelEnglish,
|
|
10
|
+
widgetParameterOptionKey,
|
|
11
|
+
widgetParameterTitleKey,
|
|
10
12
|
type PrerenderedWidgetStates,
|
|
11
13
|
type WidgetLabel,
|
|
12
14
|
} from '@use-voltra/expo-plugin'
|
|
13
15
|
|
|
14
16
|
import { DEFAULT_WIDGET_FAMILIES, WIDGET_FAMILY_MAP, widgetKind } from '../../constants'
|
|
15
|
-
import type { IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
|
|
17
|
+
import type { AppIntentParameter, IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
|
|
16
18
|
import { VOLTRA_WIDGET_STRINGS_BASENAME } from '../../utils/fileDiscovery'
|
|
17
19
|
import { detectClientRenderedWidgets, type DetectedIOSWidget } from '../clientRendered'
|
|
18
20
|
import { prerenderClientRenderedWidgets } from '../clientRenderedPrerender'
|
|
@@ -372,6 +374,67 @@ function generateWidgetStruct(widget: DetectedIOSWidget): string {
|
|
|
372
374
|
`
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Generates a Dynamic Widget backed by AppIntentConfiguration (iOS 17+): a
|
|
379
|
+
* WidgetConfigurationIntent (params + code defaults), an AppIntentTimelineProvider that loads the
|
|
380
|
+
* bundle via the shared client runtime, and an AppIntentConfiguration widget. The configured
|
|
381
|
+
* params are passed into the render as env.configuration; the native "Edit Widget" sheet edits them.
|
|
382
|
+
*/
|
|
383
|
+
/**
|
|
384
|
+
* A string literal for a title on the Edit Widget sheet (ADR 0009 §6). A plain string is compiled in
|
|
385
|
+
* as-is. A locale map becomes a bare string-literal key into the extension's default `Localizable`
|
|
386
|
+
* table, which `syncExtensionLocalizableStrings` fills for every declared language: the one form
|
|
387
|
+
* the App Intents metadata processor is documented to accept without question.
|
|
388
|
+
*/
|
|
389
|
+
function sheetStringLiteral(label: WidgetLabel, key: string): string {
|
|
390
|
+
return `"${escapeForSwiftStringLiteral(isWidgetLocalizedMap(label) ? key : label)}"`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function parameterOptionEnumName(widgetId: string, parameterName: string): string {
|
|
394
|
+
return `VoltraWidget_${widgetId}_${parameterName}_Option`
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Static `options` become an `AppEnum`, so the sheet shows a picker of localised labels instead of
|
|
399
|
+
* a free-text field. Cases are positional (`option0`…) because option values are free-form; the raw
|
|
400
|
+
* value is the option's `value`, which is what `env.configuration` receives.
|
|
401
|
+
*/
|
|
402
|
+
function generateParameterOptionEnum(widgetId: string, parameter: AppIntentParameter): string {
|
|
403
|
+
const options = parameter.options ?? []
|
|
404
|
+
const enumName = parameterOptionEnumName(widgetId, parameter.name)
|
|
405
|
+
const cases = options
|
|
406
|
+
.map((option, index) => ` case option${index} = "${escapeForSwiftStringLiteral(option.value)}"`)
|
|
407
|
+
.join('\n')
|
|
408
|
+
const representations = options
|
|
409
|
+
.map(
|
|
410
|
+
(option, index) =>
|
|
411
|
+
` .option${index}: ${sheetStringLiteral(
|
|
412
|
+
option.title,
|
|
413
|
+
widgetParameterOptionKey(widgetId, parameter.name, option.value)
|
|
414
|
+
)},`
|
|
415
|
+
)
|
|
416
|
+
.join('\n')
|
|
417
|
+
const typeTitle = sheetStringLiteral(parameter.title, widgetParameterTitleKey(widgetId, parameter.name))
|
|
418
|
+
|
|
419
|
+
return [
|
|
420
|
+
'@available(iOS 17.0, *)',
|
|
421
|
+
`enum ${enumName}: String, AppEnum {`,
|
|
422
|
+
cases,
|
|
423
|
+
'',
|
|
424
|
+
` static var typeDisplayRepresentation: TypeDisplayRepresentation = ${typeTitle}`,
|
|
425
|
+
` static var caseDisplayRepresentations: [${enumName}: DisplayRepresentation] = [`,
|
|
426
|
+
representations,
|
|
427
|
+
' ]',
|
|
428
|
+
'}',
|
|
429
|
+
].join('\n')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Index of the option `parameter.default` names, or the first option. */
|
|
433
|
+
function defaultOptionIndex(parameter: AppIntentParameter): number {
|
|
434
|
+
const index = (parameter.options ?? []).findIndex((option) => option.value === parameter.default)
|
|
435
|
+
return index >= 0 ? index : 0
|
|
436
|
+
}
|
|
437
|
+
|
|
375
438
|
/**
|
|
376
439
|
* Generates a Dynamic Widget backed by AppIntentConfiguration (iOS 17+): a
|
|
377
440
|
* WidgetConfigurationIntent (params + code defaults), an AppIntentTimelineProvider that loads the
|
|
@@ -388,20 +451,35 @@ function generateClientAppIntentWidgetCode(widget: DetectedIOSWidget): string {
|
|
|
388
451
|
const displayNameExpr = iosWidgetGalleryLabelSwiftExpr(widget.id, 'displayName', widget.displayName)
|
|
389
452
|
const descriptionExpr = iosWidgetGalleryLabelSwiftExpr(widget.id, 'description', widget.description)
|
|
390
453
|
|
|
391
|
-
const
|
|
454
|
+
const hasOptions = (p: AppIntentParameter) => (p.options?.length ?? 0) > 0
|
|
455
|
+
// The raw default, as env.configuration sees it before the user configures anything.
|
|
456
|
+
const swiftDefault = (p: AppIntentParameter) =>
|
|
457
|
+
hasOptions(p)
|
|
458
|
+
? `"${escapeForSwiftStringLiteral(p.options![defaultOptionIndex(p)]!.value)}"`
|
|
459
|
+
: `"${escapeForSwiftStringLiteral(p.default ?? '')}"`
|
|
460
|
+
const swiftType = (p: AppIntentParameter) => (hasOptions(p) ? parameterOptionEnumName(widget.id, p.name) : 'String')
|
|
461
|
+
const parameterDefault = (p: AppIntentParameter) =>
|
|
462
|
+
hasOptions(p) ? `.option${defaultOptionIndex(p)}` : swiftDefault(p)
|
|
463
|
+
const configuredValue = (p: AppIntentParameter) =>
|
|
464
|
+
hasOptions(p) ? `configuration.${p.name}.rawValue` : `configuration.${p.name}`
|
|
392
465
|
const dictLiteral = (entries: string[]) => (entries.length > 0 ? `[${entries.join(', ')}]` : '[:]')
|
|
393
466
|
|
|
467
|
+
const optionEnums = params
|
|
468
|
+
.filter(hasOptions)
|
|
469
|
+
.map((p) => generateParameterOptionEnum(widget.id, p))
|
|
470
|
+
.join('\n\n')
|
|
394
471
|
const paramDecls = params
|
|
395
472
|
.map(
|
|
396
473
|
(p) =>
|
|
397
|
-
` @Parameter(title:
|
|
398
|
-
p.
|
|
399
|
-
|
|
474
|
+
` @Parameter(title: ${sheetStringLiteral(
|
|
475
|
+
p.title,
|
|
476
|
+
widgetParameterTitleKey(widget.id, p.name)
|
|
477
|
+
)}, default: ${parameterDefault(p)})\n var ${p.name}: ${swiftType(p)}`
|
|
400
478
|
)
|
|
401
479
|
.join('\n\n')
|
|
402
|
-
const initParams = params.map((p) => `${p.name}:
|
|
480
|
+
const initParams = params.map((p) => `${p.name}: ${swiftType(p)}`).join(', ')
|
|
403
481
|
const initBody = params.map((p) => ` self.${p.name} = ${p.name}`).join('\n')
|
|
404
|
-
const configuredDict = dictLiteral(params.map((p) => `"${p.name}":
|
|
482
|
+
const configuredDict = dictLiteral(params.map((p) => `"${p.name}": ${configuredValue(p)}`))
|
|
405
483
|
const defaultDict = dictLiteral(params.map((p) => `"${p.name}": ${swiftDefault(p)}`))
|
|
406
484
|
// A server-driven Dynamic Widget fetches on every timeline request and schedules the next one
|
|
407
485
|
// from its resolved interval; a plain one has nothing to ask again for, so its policy is .never.
|
|
@@ -418,7 +496,7 @@ function generateClientAppIntentWidgetCode(widget: DetectedIOSWidget): string {
|
|
|
418
496
|
|
|
419
497
|
return dedent`
|
|
420
498
|
// MARK: - Client-rendered AppIntent widget: ${widget.id}
|
|
421
|
-
|
|
499
|
+
${optionEnums ? `\n${optionEnums}\n` : ''}
|
|
422
500
|
@available(iOS 17.0, *)
|
|
423
501
|
struct ${intentName}: WidgetConfigurationIntent {
|
|
424
502
|
static var title: LocalizedStringResource = "${intentTitle}"
|
|
@@ -518,7 +596,7 @@ function generateWidgetBundleSwift(
|
|
|
518
596
|
|
|
519
597
|
${foundationImport}${appIntentsImport}import SwiftUI
|
|
520
598
|
import WidgetKit
|
|
521
|
-
import
|
|
599
|
+
import VoltraWidgetRuntime
|
|
522
600
|
|
|
523
601
|
@main
|
|
524
602
|
struct VoltraWidgetBundle: WidgetBundle {
|
|
@@ -552,7 +630,7 @@ function generateDefaultWidgetBundleSwift(): string {
|
|
|
552
630
|
|
|
553
631
|
import SwiftUI
|
|
554
632
|
import WidgetKit
|
|
555
|
-
import
|
|
633
|
+
import VoltraWidgetRuntime // Import Voltra widgets
|
|
556
634
|
|
|
557
635
|
@main
|
|
558
636
|
struct VoltraWidgetBundle: WidgetBundle {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { XcodeProject } from '@expo/config-plugins'
|
|
2
2
|
|
|
3
3
|
import type { IOSWidgetExtensionFiles } from '../../types'
|
|
4
|
-
import { ensureWidgetFileReference } from './fileReferences'
|
|
4
|
+
import { ensureWidgetFileReference, normalizeRef } from './fileReferences'
|
|
5
5
|
|
|
6
6
|
const pbxFile = require('xcode/lib/pbxFile')
|
|
7
7
|
|
|
@@ -89,8 +89,11 @@ export function ensurePbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupO
|
|
|
89
89
|
for (const filePath of allFiles) {
|
|
90
90
|
const file = new pbxFile(filePath)
|
|
91
91
|
const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
|
|
92
|
+
// Localized files share a basename across `.lproj` folders (`pl.lproj/Localizable.strings`,
|
|
93
|
+
// `de.lproj/Localizable.strings`), so for them only the file reference itself identifies the child.
|
|
94
|
+
const isLocalized = filePath.includes('.lproj/')
|
|
92
95
|
const alreadyInGroup = existingGroup.children.some(
|
|
93
|
-
(child: any) => child.value === fileRef || child.comment === file.basename
|
|
96
|
+
(child: any) => normalizeRef(child.value) === fileRef || (!isLocalized && child.comment === file.basename)
|
|
94
97
|
)
|
|
95
98
|
if (!alreadyInGroup) {
|
|
96
99
|
existingGroup.children.push({ value: fileRef, comment: file.basename })
|
package/expo-plugin/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ConfigPlugin } from '@expo/config-plugins'
|
|
|
3
3
|
import type {
|
|
4
4
|
DynamicLiveActivityEntryConfig,
|
|
5
5
|
DynamicWidgetEntryConfig,
|
|
6
|
+
WidgetConfigurationOption,
|
|
6
7
|
WidgetInitialStatePath,
|
|
7
8
|
WidgetLabel,
|
|
8
9
|
} from '@use-voltra/expo-plugin'
|
|
@@ -25,10 +26,19 @@ export type IOSWidgetFamily =
|
|
|
25
26
|
export interface AppIntentParameter {
|
|
26
27
|
/** Swift property name + the key under `env.configuration`. */
|
|
27
28
|
name: string
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Label shown in the widget configuration sheet: a plain string, or a locale map
|
|
31
|
+
* (`{ "en": "Units", "pl": "Jednostki" }`) written to the extension's `Localizable.strings`.
|
|
32
|
+
*/
|
|
33
|
+
title: WidgetLabel
|
|
34
|
+
/** Default value used before the user configures the widget (the "from code" default). Must be
|
|
35
|
+
* one of `options[].value` when `options` is set. */
|
|
31
36
|
default?: string
|
|
37
|
+
/**
|
|
38
|
+
* Fixed values the user picks from. The sheet shows a picker with each option's (localisable)
|
|
39
|
+
* `title` instead of a free-text field; `env.configuration` receives the option's `value`.
|
|
40
|
+
*/
|
|
41
|
+
options?: WidgetConfigurationOption[]
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -7,6 +7,9 @@ import { logger } from '@use-voltra/expo-plugin'
|
|
|
7
7
|
/** Table name matches basename without extension; co-located per locale in *.lproj */
|
|
8
8
|
export const VOLTRA_WIDGET_STRINGS_BASENAME = 'VoltraWidgets.strings'
|
|
9
9
|
|
|
10
|
+
/** Default table: Edit Widget sheet strings, and the file that makes each `.lproj` a real resource. */
|
|
11
|
+
export const VOLTRA_LOCALIZABLE_STRINGS_BASENAME = 'Localizable.strings'
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* Scans the widget extension target directory and returns categorized file lists.
|
|
12
15
|
*
|
|
@@ -43,9 +46,10 @@ export function getIOSWidgetExtensionFiles(targetPath: string, targetName: strin
|
|
|
43
46
|
if (!entry.endsWith('.lproj')) {
|
|
44
47
|
continue
|
|
45
48
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
for (const basename of [VOLTRA_WIDGET_STRINGS_BASENAME, VOLTRA_LOCALIZABLE_STRINGS_BASENAME]) {
|
|
50
|
+
if (fs.existsSync(path.join(targetPath, entry, basename))) {
|
|
51
|
+
widgetFiles.localizedStringResources.push(`${entry}/${basename}`)
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
validateHomeScreenWidgetId,
|
|
3
3
|
validateInitialStatePath,
|
|
4
|
+
validateWidgetConfigurationCopy,
|
|
4
5
|
validateWidgetEntry,
|
|
5
6
|
validateWidgetLabel,
|
|
6
7
|
validateWidgetServerUpdate,
|
|
@@ -55,6 +56,7 @@ export function validateIOSWidgetConfig(widget: IOSWidgetConfig, projectRoot?: s
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
validateWidgetServerUpdate(widget.serverUpdate, widget.id, iosServerUpdateRules(widget))
|
|
59
|
+
validateWidgetConfigurationCopy(widget, { requireParameterTitle: true })
|
|
58
60
|
|
|
59
61
|
if (widget.supportedFamilies) {
|
|
60
62
|
if (!Array.isArray(widget.supportedFamilies)) {
|