@microsoft/power-apps-native-host 0.2.25 → 0.3.3

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.
Files changed (154) hide show
  1. package/README.md +21 -10
  2. package/compatibility/current.json +31 -4
  3. package/lib/NOTICE.txt +1325 -24
  4. package/lib/commonjs/auth/AuthContext.js +110 -33
  5. package/lib/commonjs/bridge/NativePowerAppsBridge.js +11 -18
  6. package/lib/commonjs/bridge/PowerPagesBridge.js +27 -8
  7. package/lib/commonjs/bridge/bridgeState.js +40 -0
  8. package/lib/commonjs/config/babelConfig.js +13 -0
  9. package/lib/commonjs/config/expoConfig.js +71 -0
  10. package/lib/commonjs/config/metroConfig.js +70 -0
  11. package/lib/commonjs/config/metroLogger.js +182 -0
  12. package/lib/commonjs/config/tamaguiConfig.js +98 -0
  13. package/lib/commonjs/connections/ConnectionSetupScreen.js +459 -80
  14. package/lib/commonjs/connections/PowerPlatformConnectionApi.js +946 -0
  15. package/lib/commonjs/connections/connectionMetadata.js +127 -0
  16. package/lib/commonjs/connections/connectionState.js +96 -0
  17. package/lib/commonjs/connections/disambiguate.js +19 -11
  18. package/lib/commonjs/connections/resolveConnections.js +348 -0
  19. package/lib/commonjs/connections/types.js +106 -0
  20. package/lib/commonjs/{platform → connections}/useConnectionRefs.js +130 -65
  21. package/lib/commonjs/connections/useConnectionSetup.js +111 -52
  22. package/lib/commonjs/controls/FilePicker.js +6 -0
  23. package/lib/commonjs/controls/ImagePicker.js +8 -0
  24. package/lib/commonjs/index.js +167 -4
  25. package/lib/commonjs/logger.js +9 -6
  26. package/lib/commonjs/offline/createOfflineHostActivationCallbacks.js +61 -0
  27. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.js +48 -0
  28. package/lib/commonjs/offline/useOfflineIntegrationWithStartupGate.web.js +13 -0
  29. package/lib/commonjs/platform/DataverseDataSources.js +18 -0
  30. package/lib/commonjs/platform/DataverseGlobalDiscoveryApi.js +94 -0
  31. package/lib/commonjs/platform/GlobalDiscovery.js +51 -0
  32. package/lib/commonjs/platform/PowerPlatformEnvironmentApi.js +6 -106
  33. package/lib/commonjs/provider/PowerAppsHostProvider.js +157 -25
  34. package/lib/commonjs/provider/PowerAppsProvider.js +178 -134
  35. package/lib/commonjs/provider/SessionDetailsGesture.js +208 -0
  36. package/lib/commonjs/telemetry/OfflineSdkTelemetry.js +95 -0
  37. package/lib/commonjs/telemetry/OfflineSdkTelemetry.web.js +9 -0
  38. package/lib/commonjs/telemetry/index.js +22 -3
  39. package/lib/module/auth/AuthContext.js +110 -33
  40. package/lib/module/bridge/NativePowerAppsBridge.js +12 -19
  41. package/lib/module/bridge/PowerPagesBridge.js +27 -8
  42. package/lib/module/bridge/bridgeState.js +34 -0
  43. package/lib/module/config/babelConfig.js +9 -0
  44. package/lib/module/config/expoConfig.js +67 -0
  45. package/lib/module/config/metroConfig.js +65 -0
  46. package/lib/module/config/metroLogger.js +175 -0
  47. package/lib/module/config/tamaguiConfig.js +93 -0
  48. package/lib/module/connections/ConnectionSetupScreen.js +460 -81
  49. package/lib/module/connections/PowerPlatformConnectionApi.js +932 -0
  50. package/lib/module/connections/connectionMetadata.js +112 -0
  51. package/lib/module/connections/connectionState.js +82 -0
  52. package/lib/module/connections/disambiguate.js +19 -12
  53. package/lib/module/connections/resolveConnections.js +343 -0
  54. package/lib/module/connections/types.js +91 -0
  55. package/lib/module/{platform → connections}/useConnectionRefs.js +131 -66
  56. package/lib/module/connections/useConnectionSetup.js +113 -54
  57. package/lib/module/controls/FilePicker.js +6 -0
  58. package/lib/module/controls/ImagePicker.js +8 -0
  59. package/lib/module/index.js +14 -4
  60. package/lib/module/logger.js +8 -6
  61. package/lib/module/offline/createOfflineHostActivationCallbacks.js +55 -0
  62. package/lib/module/offline/useOfflineIntegrationWithStartupGate.js +44 -0
  63. package/lib/module/offline/useOfflineIntegrationWithStartupGate.web.js +9 -0
  64. package/lib/module/platform/DataverseDataSources.js +13 -0
  65. package/lib/module/platform/DataverseGlobalDiscoveryApi.js +89 -0
  66. package/lib/module/platform/GlobalDiscovery.js +45 -0
  67. package/lib/module/platform/PowerPlatformEnvironmentApi.js +5 -104
  68. package/lib/module/provider/PowerAppsHostProvider.js +160 -28
  69. package/lib/module/provider/PowerAppsProvider.js +182 -138
  70. package/lib/module/provider/SessionDetailsGesture.js +203 -0
  71. package/lib/module/telemetry/OfflineSdkTelemetry.js +91 -0
  72. package/lib/module/telemetry/OfflineSdkTelemetry.web.js +5 -0
  73. package/lib/module/telemetry/index.js +15 -4
  74. package/lib/typescript/commonjs/auth/AuthContext.d.ts +3 -1
  75. package/lib/typescript/commonjs/auth/AuthContext.web.d.ts +1 -0
  76. package/lib/typescript/commonjs/bridge/bridgeState.d.ts +5 -0
  77. package/lib/typescript/commonjs/config/babelConfig.d.ts +12 -0
  78. package/lib/typescript/commonjs/config/expoConfig.d.ts +21 -0
  79. package/lib/typescript/commonjs/config/metroConfig.d.ts +6 -0
  80. package/lib/typescript/commonjs/config/metroLogger.d.ts +21 -0
  81. package/lib/typescript/commonjs/config/tamaguiConfig.d.ts +1792 -0
  82. package/lib/typescript/commonjs/connections/ConnectionSetupScreen.d.ts +28 -7
  83. package/lib/typescript/commonjs/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  84. package/lib/typescript/commonjs/connections/connectionMetadata.d.ts +66 -0
  85. package/lib/typescript/commonjs/connections/connectionState.d.ts +16 -0
  86. package/lib/typescript/commonjs/connections/disambiguate.d.ts +4 -2
  87. package/lib/typescript/commonjs/{platform → connections}/resolveConnections.d.ts +22 -20
  88. package/lib/typescript/commonjs/connections/types.d.ts +129 -1
  89. package/lib/typescript/commonjs/{platform → connections}/useConnectionRefs.d.ts +36 -25
  90. package/lib/typescript/commonjs/connections/useConnectionSetup.d.ts +12 -8
  91. package/lib/typescript/commonjs/controls/FilePicker.d.ts +7 -1
  92. package/lib/typescript/commonjs/controls/ImagePicker.d.ts +9 -1
  93. package/lib/typescript/commonjs/index.d.ts +18 -10
  94. package/lib/typescript/commonjs/logger.d.ts +4 -4
  95. package/lib/typescript/commonjs/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  96. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  97. package/lib/typescript/commonjs/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  98. package/lib/typescript/commonjs/platform/DataverseDataSources.d.ts +28 -0
  99. package/lib/typescript/commonjs/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  100. package/lib/typescript/commonjs/platform/GlobalDiscovery.d.ts +4 -0
  101. package/lib/typescript/commonjs/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  102. package/lib/typescript/commonjs/provider/PowerAppsHostProvider.d.ts +29 -18
  103. package/lib/typescript/commonjs/provider/PowerAppsProvider.d.ts +54 -9
  104. package/lib/typescript/commonjs/provider/SessionDetailsGesture.d.ts +13 -0
  105. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  106. package/lib/typescript/commonjs/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  107. package/lib/typescript/commonjs/telemetry/index.d.ts +13 -1
  108. package/lib/typescript/module/auth/AuthContext.d.ts +3 -1
  109. package/lib/typescript/module/auth/AuthContext.web.d.ts +1 -0
  110. package/lib/typescript/module/bridge/bridgeState.d.ts +5 -0
  111. package/lib/typescript/module/config/babelConfig.d.ts +12 -0
  112. package/lib/typescript/module/config/expoConfig.d.ts +21 -0
  113. package/lib/typescript/module/config/metroConfig.d.ts +6 -0
  114. package/lib/typescript/module/config/metroLogger.d.ts +21 -0
  115. package/lib/typescript/module/config/tamaguiConfig.d.ts +1792 -0
  116. package/lib/typescript/module/connections/ConnectionSetupScreen.d.ts +28 -7
  117. package/lib/typescript/module/{platform → connections}/PowerPlatformConnectionApi.d.ts +52 -43
  118. package/lib/typescript/module/connections/connectionMetadata.d.ts +66 -0
  119. package/lib/typescript/module/connections/connectionState.d.ts +16 -0
  120. package/lib/typescript/module/connections/disambiguate.d.ts +4 -2
  121. package/lib/typescript/module/{platform → connections}/resolveConnections.d.ts +22 -20
  122. package/lib/typescript/module/connections/types.d.ts +129 -1
  123. package/lib/typescript/module/{platform → connections}/useConnectionRefs.d.ts +36 -25
  124. package/lib/typescript/module/connections/useConnectionSetup.d.ts +12 -8
  125. package/lib/typescript/module/controls/FilePicker.d.ts +7 -1
  126. package/lib/typescript/module/controls/ImagePicker.d.ts +9 -1
  127. package/lib/typescript/module/index.d.ts +18 -10
  128. package/lib/typescript/module/logger.d.ts +4 -4
  129. package/lib/typescript/module/offline/createOfflineHostActivationCallbacks.d.ts +12 -0
  130. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.d.ts +15 -0
  131. package/lib/typescript/module/offline/useOfflineIntegrationWithStartupGate.web.d.ts +6 -0
  132. package/lib/typescript/module/platform/DataverseDataSources.d.ts +28 -0
  133. package/lib/typescript/module/platform/DataverseGlobalDiscoveryApi.d.ts +3 -0
  134. package/lib/typescript/module/platform/GlobalDiscovery.d.ts +4 -0
  135. package/lib/typescript/module/platform/PowerPlatformEnvironmentApi.d.ts +3 -74
  136. package/lib/typescript/module/provider/PowerAppsHostProvider.d.ts +29 -18
  137. package/lib/typescript/module/provider/PowerAppsProvider.d.ts +54 -9
  138. package/lib/typescript/module/provider/SessionDetailsGesture.d.ts +13 -0
  139. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.d.ts +15 -0
  140. package/lib/typescript/module/telemetry/OfflineSdkTelemetry.web.d.ts +2 -0
  141. package/lib/typescript/module/telemetry/index.d.ts +13 -1
  142. package/package.json +52 -13
  143. package/scripts/build-codegen-package.js +283 -0
  144. package/scripts/start-web.mjs +9 -5
  145. package/scripts/upgrade/runtime.js +139 -24
  146. package/scripts/upgrade-authoring.js +274 -180
  147. package/scripts/upgrade.js +10 -16
  148. package/src/config/generated-config.d.ts +4 -0
  149. package/src/config/index.d.ts +3 -0
  150. package/src/config/tsconfig.json +26 -0
  151. package/lib/commonjs/platform/PowerPlatformConnectionApi.js +0 -606
  152. package/lib/commonjs/platform/resolveConnections.js +0 -264
  153. package/lib/module/platform/PowerPlatformConnectionApi.js +0 -595
  154. package/lib/module/platform/resolveConnections.js +0 -259
@@ -42,14 +42,16 @@
42
42
  */
43
43
 
44
44
  import React, { useState, useEffect, useCallback } from 'react';
45
- import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet, ScrollView } from 'react-native';
45
+ import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet, ScrollView, Switch, TextInput } from 'react-native';
46
46
  import * as WebBrowser from 'expo-web-browser';
47
47
  import * as Linking from 'expo-linking';
48
48
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
49
49
  import { useConnectionSetup } from "../connections/useConnectionSetup.js";
50
- import { fetchConnectorMetadata, fetchConnectionsForConnector } from "../platform/PowerPlatformConnectionApi.js";
50
+ import { fetchConnectorMetadata, fetchConnectionsForConnector } from "./PowerPlatformConnectionApi.js";
51
51
  import { powerPlatformScope } from "../platform/PowerPlatformEnvironmentApi.js";
52
52
  import { ConnectionStatus } from "../connections/types.js";
53
+ import { normalizeApiId } from "./types.js";
54
+ import { connectionIdsEqual, extractConnectionId } from "./disambiguate.js";
53
55
 
54
56
  // ---------------------------------------------------------------------------
55
57
  // Types
@@ -61,12 +63,17 @@ import { ConnectionStatus } from "../connections/types.js";
61
63
  // ---------------------------------------------------------------------------
62
64
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
63
65
  function ConnectorSetupStep({
66
+ localRefId,
64
67
  connectorId,
65
68
  apiId,
66
69
  environmentId,
67
70
  region,
68
71
  acquireToken,
72
+ onSignOut,
69
73
  isvClientId,
74
+ gatewayObjectIdHint,
75
+ parameterSetName,
76
+ sharedConnectionId,
70
77
  onConnected,
71
78
  renderOAuthWebView
72
79
  }) {
@@ -80,18 +87,24 @@ function ConnectorSetupStep({
80
87
  // useConnectionSetup) so the hook stays focused on connection flow only.
81
88
  const [isLoadingMetadata, setIsLoadingMetadata] = useState(true);
82
89
  const [metadata, setMetadata] = useState(null);
90
+ const [metadataError, setMetadataError] = useState(null);
91
+ const [metadataReload, setMetadataReload] = useState(0);
83
92
  const [showOAuthModal, setShowOAuthModal] = useState(false);
84
- // Track if there's already a connected instance the user can reuse.
85
- const [existingConnected, setExistingConnected] = useState(null);
93
+ const [existingConnected, setExistingConnected] = useState([]);
94
+ const [existingRepairable, setExistingRepairable] = useState([]);
95
+ const [selectedParameterSet, setSelectedParameterSet] = useState(parameterSetName ?? '');
96
+ const [parameterValues, setParameterValues] = useState({});
97
+ const [gatewayObjectId, setGatewayObjectId] = useState(gatewayObjectIdHint ?? '');
86
98
  const setup = useConnectionSetup({
87
99
  connectorId,
88
100
  environmentId,
89
101
  region,
90
102
  acquireToken,
91
103
  isvClientId,
104
+ sharedConnectionId,
92
105
  onConnected: instance => {
93
106
  setShowOAuthModal(false);
94
- onConnected(apiId, instance);
107
+ onConnected(localRefId, instance);
95
108
  },
96
109
  oauthRedirectUrl,
97
110
  polling: {
@@ -103,46 +116,94 @@ function ConnectorSetupStep({
103
116
  // Fetch connector metadata and check for existing non-connected connections.
104
117
  useEffect(() => {
105
118
  let cancelled = false;
119
+ setIsLoadingMetadata(true);
120
+ setMetadata(null);
121
+ setMetadataError(null);
122
+ setExistingConnected([]);
123
+ setExistingRepairable([]);
124
+ setSelectedParameterSet(parameterSetName ?? '');
125
+ setParameterValues({});
126
+ setGatewayObjectId(gatewayObjectIdHint ?? '');
106
127
  async function load() {
107
128
  const token = await acquireToken([powerPlatformScope(region)]);
108
129
  if (!token || cancelled) {
109
130
  if (!cancelled) {
110
- setMetadata({
111
- displayName: connectorId,
112
- iconUri: '',
113
- description: ''
114
- });
131
+ setMetadataError('Unable to acquire a token to load connector setup.');
115
132
  setIsLoadingMetadata(false);
116
133
  }
117
134
  return;
118
135
  }
119
- const [meta, instances] = await Promise.all([fetchConnectorMetadata(connectorId, environmentId, region, token), fetchConnectionsForConnector(connectorId, environmentId, region, token)]);
120
- if (cancelled) return;
121
- setMetadata(meta ?? {
122
- displayName: connectorId,
123
- iconUri: '',
124
- description: ''
125
- });
136
+ try {
137
+ const [meta, instances] = await Promise.all([fetchConnectorMetadata(connectorId, environmentId, region, token), fetchConnectionsForConnector(connectorId, environmentId, region, token)]);
138
+ if (cancelled) return;
139
+ if (!meta) throw new Error('Connector metadata is unavailable.');
140
+ const resolvedMetadata = meta;
141
+ setMetadata(resolvedMetadata);
142
+ const preferredSet = resolvedMetadata.parameterSets.find(set => set.name === parameterSetName) ?? resolvedMetadata.parameterSets[0];
143
+ setSelectedParameterSet(preferredSet?.name ?? '');
144
+ setParameterValues(Object.fromEntries((preferredSet?.parameters ?? []).filter(parameter => parameter.defaultValue !== undefined).map(parameter => [parameter.name, parameter.defaultValue])));
126
145
 
127
- // If there's already a Connected instance the disambiguation didn't pick
128
- // (e.g., appeared after the initial load), use it directly.
129
- const connected = instances.find(i => i.status === ConnectionStatus.Connected);
130
- if (connected) setExistingConnected(connected);
131
- setIsLoadingMetadata(false);
146
+ // If there's already a Connected instance the disambiguation didn't pick
147
+ // (e.g., appeared after the initial load), use it directly.
148
+ const sharedId = sharedConnectionId ? extractConnectionId(sharedConnectionId) : undefined;
149
+ const eligibleInstances = sharedId ? instances.filter(instance => connectionIdsEqual(instance.id, sharedId)) : instances;
150
+ setExistingConnected(eligibleInstances.filter(instance => instance.status === ConnectionStatus.Connected));
151
+ setExistingRepairable(sharedId ? [] : eligibleInstances.filter(instance => instance.status !== ConnectionStatus.Connected));
152
+ } catch (error) {
153
+ if (!cancelled) {
154
+ setMetadataError(error instanceof Error ? error.message : 'Unable to load connector setup.');
155
+ }
156
+ } finally {
157
+ if (!cancelled) setIsLoadingMetadata(false);
158
+ }
132
159
  }
133
160
  void load();
134
161
  return () => {
135
162
  cancelled = true;
136
163
  };
137
- // eslint-disable-next-line react-hooks/exhaustive-deps
138
- }, [connectorId]);
139
- const handleUseExisting = useCallback(() => {
140
- if (existingConnected) {
141
- onConnected(apiId, existingConnected);
164
+ }, [acquireToken, connectorId, environmentId, gatewayObjectIdHint, parameterSetName, region, sharedConnectionId, metadataReload]);
165
+ const handleUseExisting = useCallback(async instance => {
166
+ await setup.useExisting(instance);
167
+ }, [setup]);
168
+ const activeParameterSet = metadata?.parameterSets.find(set => (set.name ?? '') === selectedParameterSet) ?? metadata?.parameterSets[0];
169
+ const selectedCapability = gatewayObjectIdHint ? 'gateway' : 'cloud';
170
+ const visibleParameters = activeParameterSet?.parameters.filter(parameter => !parameter.hidden && (!parameter.capabilities?.length || parameter.capabilities.some(capability => capability.toLowerCase() === selectedCapability))) ?? [];
171
+ const requiredParametersComplete = visibleParameters.every(parameter => {
172
+ if (!parameter.required) return true;
173
+ const value = parameterValues[parameter.name];
174
+ if (typeof value === 'object' && value && 'pfx' in value) {
175
+ return Boolean(value.pfx && value.password);
142
176
  }
143
- }, [existingConnected, apiId, onConnected]);
177
+ return value !== undefined && value !== '';
178
+ });
179
+ const buildInput = useCallback(() => ({
180
+ parameterSetName: activeParameterSet?.name,
181
+ gatewayObjectId: gatewayObjectId || undefined,
182
+ gatewayParameterName: activeParameterSet?.gatewayParameterName,
183
+ parameters: parameterValues
184
+ }), [activeParameterSet?.name, gatewayObjectId, parameterValues]);
144
185
  const handleConnect = useCallback(async () => {
145
- await setup.startCreate();
186
+ await setup.startCreate(buildInput());
187
+ }, [setup, buildInput]);
188
+ const handleRepair = useCallback(async instance => {
189
+ const instanceParameterSet = metadata?.parameterSets.find(parameterSet => parameterSet.name === instance.parameterSetName) ?? activeParameterSet;
190
+ const requiresApiConsent = instanceParameterSet?.requiresApiConsent ?? metadata?.hasOAuthParameter ?? false;
191
+ const input = buildInput();
192
+ const hasEditableInput = Object.keys(input.parameters).length > 0 || Boolean(input.gatewayObjectId);
193
+ await setup.startRepair(instance, !requiresApiConsent || hasEditableInput ? input : undefined, requiresApiConsent);
194
+ }, [setup, metadata, activeParameterSet, buildInput]);
195
+ const completeOAuthRedirect = useCallback(async redirectUrl => {
196
+ let code;
197
+ if (redirectUrl) {
198
+ try {
199
+ const raw = new URL(redirectUrl).searchParams.get('code') ?? '';
200
+ if (/^[0-9a-f]{32}$/i.test(raw)) code = raw;
201
+ } catch {
202
+ setup.reset();
203
+ return;
204
+ }
205
+ }
206
+ await setup.completeConsent(code);
146
207
  }, [setup]);
147
208
 
148
209
  // Open OAuth UI when consentUrl becomes available (set by startCreate in the hook).
@@ -156,29 +217,24 @@ function ConnectorSetupStep({
156
217
  // custom-scheme redirect (e.g. expo-app://oauth-callback).
157
218
  void (async () => {
158
219
  const result = await WebBrowser.openAuthSessionAsync(setup.consentUrl, oauthRedirectUrl);
159
- // Extract a CommitTokenCode from the redirect URL if present.
160
- let code;
161
- if (result.type === 'success' && result.url) {
162
- const params = new URL(result.url).searchParams;
163
- const raw = params.get('code') ?? '';
164
- // CommitTokenCode is a 32-char lowercase hex string.
165
- if (/^[0-9a-f]{32}$/i.test(raw)) {
166
- code = raw;
167
- }
220
+ if (result.type === 'success') {
221
+ await completeOAuthRedirect(result.url);
222
+ } else {
223
+ setup.reset();
168
224
  }
169
- await setup.completeConsent(code);
170
225
  })();
171
226
  }
172
227
  }
173
- }, [setup.phase, setup.consentUrl, renderOAuthWebView, oauthRedirectUrl]);
174
- const handleWebViewDismiss = useCallback(async () => {
228
+ }, [setup.phase, setup.consentUrl, renderOAuthWebView, oauthRedirectUrl, completeOAuthRedirect]);
229
+ const handleWebViewDismiss = useCallback(async redirectUrl => {
175
230
  setShowOAuthModal(false);
176
- await setup.checkNow();
177
- }, [setup]);
231
+ if (redirectUrl) await completeOAuthRedirect(redirectUrl);else await setup.checkNow();
232
+ }, [completeOAuthRedirect, setup]);
178
233
  const displayName = metadata?.displayName ?? connectorId;
179
234
  const description = metadata?.description ?? '';
180
235
  if (isLoadingMetadata) {
181
236
  return /*#__PURE__*/_jsx(View, {
237
+ testID: `powerApps.connectionSetup.connector.${connectorId}`,
182
238
  style: styles.connectorCard,
183
239
  children: /*#__PURE__*/_jsx(View, {
184
240
  style: styles.metadataLoading,
@@ -188,7 +244,36 @@ function ConnectorSetupStep({
188
244
  })
189
245
  });
190
246
  }
247
+ if (metadataError || !metadata) {
248
+ return /*#__PURE__*/_jsxs(View, {
249
+ style: styles.connectorCard,
250
+ children: [/*#__PURE__*/_jsx(Text, {
251
+ style: styles.connectorName,
252
+ children: connectorId
253
+ }), /*#__PURE__*/_jsx(Text, {
254
+ style: styles.errorText,
255
+ children: metadataError ?? 'Unable to load connector setup.'
256
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
257
+ style: styles.button,
258
+ onPress: () => setMetadataReload(value => value + 1),
259
+ children: /*#__PURE__*/_jsx(Text, {
260
+ style: styles.buttonText,
261
+ children: "Retry"
262
+ })
263
+ }), onSignOut ? /*#__PURE__*/_jsx(TouchableOpacity, {
264
+ style: [styles.button, styles.buttonSecondary],
265
+ onPress: () => {
266
+ void onSignOut();
267
+ },
268
+ children: /*#__PURE__*/_jsx(Text, {
269
+ style: styles.buttonTextSecondary,
270
+ children: "Sign out"
271
+ })
272
+ }) : null]
273
+ });
274
+ }
191
275
  return /*#__PURE__*/_jsxs(View, {
276
+ testID: `powerApps.connectionSetup.connector.${connectorId}`,
192
277
  style: styles.connectorCard,
193
278
  children: [/*#__PURE__*/_jsx(Text, {
194
279
  style: styles.connectorName,
@@ -196,33 +281,140 @@ function ConnectorSetupStep({
196
281
  }), description ? /*#__PURE__*/_jsx(Text, {
197
282
  style: styles.connectorDescription,
198
283
  children: description
199
- }) : null, setup.phase === 'idle' && /*#__PURE__*/_jsx(_Fragment, {
200
- children: existingConnected ? /*#__PURE__*/_jsxs(View, {
201
- style: styles.buttonRow,
202
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
203
- style: [styles.button, styles.buttonSecondary],
204
- onPress: handleUseExisting,
284
+ }) : null, setup.phase === 'idle' && !sharedConnectionId && (metadata?.parameterSets.length ?? 0) > 1 ? /*#__PURE__*/_jsxs(View, {
285
+ style: styles.fieldGroup,
286
+ children: [/*#__PURE__*/_jsx(Text, {
287
+ style: styles.fieldLabel,
288
+ children: "Authentication type"
289
+ }), /*#__PURE__*/_jsx(View, {
290
+ style: styles.choiceRow,
291
+ children: metadata?.parameterSets.map(parameterSet => /*#__PURE__*/_jsx(TouchableOpacity, {
292
+ style: [styles.choiceButton, selectedParameterSet === (parameterSet.name ?? '') && styles.choiceButtonSelected],
293
+ onPress: () => {
294
+ setSelectedParameterSet(parameterSet.name ?? '');
295
+ setParameterValues(Object.fromEntries(parameterSet.parameters.filter(parameter => parameter.defaultValue !== undefined).map(parameter => [parameter.name, parameter.defaultValue])));
296
+ },
205
297
  children: /*#__PURE__*/_jsx(Text, {
206
- style: styles.buttonTextSecondary,
207
- children: "Use existing"
298
+ style: selectedParameterSet === (parameterSet.name ?? '') ? styles.choiceTextSelected : styles.choiceText,
299
+ children: parameterSet.displayName
208
300
  })
209
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
210
- style: styles.button,
211
- onPress: handleConnect,
301
+ }, parameterSet.name ?? 'default'))
302
+ })]
303
+ }) : null, setup.phase === 'idle' && !sharedConnectionId && visibleParameters.map(parameter => /*#__PURE__*/_jsxs(View, {
304
+ style: styles.fieldGroup,
305
+ children: [/*#__PURE__*/_jsxs(Text, {
306
+ style: styles.fieldLabel,
307
+ children: [parameter.displayName, parameter.required ? ' *' : '']
308
+ }), parameter.allowedValues?.length ? /*#__PURE__*/_jsx(View, {
309
+ style: styles.choiceRow,
310
+ children: parameter.allowedValues.map(option => /*#__PURE__*/_jsx(TouchableOpacity, {
311
+ style: [styles.choiceButton, parameterValues[parameter.name] === option.value && styles.choiceButtonSelected],
312
+ onPress: () => setParameterValues(values => ({
313
+ ...values,
314
+ [parameter.name]: option.value
315
+ })),
212
316
  children: /*#__PURE__*/_jsx(Text, {
213
- style: styles.buttonText,
214
- children: "Create new"
317
+ style: parameterValues[parameter.name] === option.value ? styles.choiceTextSelected : styles.choiceText,
318
+ children: option.displayName
215
319
  })
320
+ }, option.value))
321
+ }) : parameter.type === 'boolean' ? /*#__PURE__*/_jsx(Switch, {
322
+ value: Boolean(parameterValues[parameter.name]),
323
+ onValueChange: value => setParameterValues(values => ({
324
+ ...values,
325
+ [parameter.name]: value
326
+ }))
327
+ }) : parameter.type === 'clientcertificate' ? /*#__PURE__*/_jsxs(_Fragment, {
328
+ children: [/*#__PURE__*/_jsx(TextInput, {
329
+ style: styles.textInput,
330
+ placeholder: "PFX certificate (base64)",
331
+ autoCapitalize: "none",
332
+ value: parameterValues[parameter.name]?.pfx ?? '',
333
+ onChangeText: pfx => setParameterValues(values => ({
334
+ ...values,
335
+ [parameter.name]: {
336
+ pfx,
337
+ password: values[parameter.name]?.password ?? ''
338
+ }
339
+ }))
340
+ }), /*#__PURE__*/_jsx(TextInput, {
341
+ style: styles.textInput,
342
+ placeholder: "Certificate password",
343
+ secureTextEntry: true,
344
+ value: parameterValues[parameter.name]?.password ?? '',
345
+ onChangeText: password => setParameterValues(values => ({
346
+ ...values,
347
+ [parameter.name]: {
348
+ pfx: values[parameter.name]?.pfx ?? '',
349
+ password
350
+ }
351
+ }))
216
352
  })]
217
- }) : /*#__PURE__*/_jsx(TouchableOpacity, {
218
- style: styles.button,
353
+ }) : /*#__PURE__*/_jsx(TextInput, {
354
+ style: styles.textInput,
355
+ secureTextEntry: parameter.type === 'securestring',
356
+ keyboardType: parameter.type === 'integer' ? 'number-pad' : 'default',
357
+ autoCapitalize: "none",
358
+ value: String(parameterValues[parameter.name] ?? ''),
359
+ onChangeText: value => setParameterValues(values => ({
360
+ ...values,
361
+ [parameter.name]: parameter.type === 'integer' && value !== '' ? /^-?\d+$/.test(value) ? parseInt(value, 10) : '' : value
362
+ }))
363
+ })]
364
+ }, parameter.name)), setup.phase === 'idle' && !sharedConnectionId && gatewayObjectIdHint ? /*#__PURE__*/_jsxs(View, {
365
+ style: styles.fieldGroup,
366
+ children: [/*#__PURE__*/_jsx(Text, {
367
+ style: styles.fieldLabel,
368
+ children: "Gateway"
369
+ }), /*#__PURE__*/_jsx(TextInput, {
370
+ style: styles.textInput,
371
+ value: gatewayObjectId,
372
+ onChangeText: setGatewayObjectId,
373
+ autoCapitalize: "none"
374
+ })]
375
+ }) : null, setup.phase === 'idle' && /*#__PURE__*/_jsxs(_Fragment, {
376
+ children: [existingConnected.map((instance, index) => /*#__PURE__*/_jsx(TouchableOpacity, {
377
+ testID: index === 0 ? `powerApps.connectionSetup.connector.${connectorId}.useExistingButton` : `powerApps.connectionSetup.connector.${connectorId}.useExistingButton.${instance.id}`,
378
+ style: [styles.button, styles.buttonSecondary],
379
+ onPress: () => handleUseExisting(instance),
380
+ children: /*#__PURE__*/_jsxs(Text, {
381
+ style: styles.buttonTextSecondary,
382
+ children: ["Use ", instance.displayName || instance.id]
383
+ })
384
+ }, instance.id)), existingRepairable.map(instance => /*#__PURE__*/_jsx(TouchableOpacity, {
385
+ style: [styles.button, styles.buttonSecondary],
386
+ disabled: !requiredParametersComplete,
387
+ onPress: () => handleRepair(instance),
388
+ children: /*#__PURE__*/_jsxs(Text, {
389
+ style: styles.buttonTextSecondary,
390
+ children: ["Fix ", instance.displayName || instance.id]
391
+ })
392
+ }, instance.id)), sharedConnectionId && existingConnected.length === 0 ? /*#__PURE__*/_jsxs(View, {
393
+ children: [/*#__PURE__*/_jsx(Text, {
394
+ style: styles.errorText,
395
+ children: "You do not have access to the shared connection required by this app."
396
+ }), onSignOut ? /*#__PURE__*/_jsx(TouchableOpacity, {
397
+ style: [styles.button, styles.buttonSecondary],
398
+ onPress: () => {
399
+ void onSignOut();
400
+ },
401
+ children: /*#__PURE__*/_jsx(Text, {
402
+ style: styles.buttonTextSecondary,
403
+ children: "Sign out"
404
+ })
405
+ }) : null]
406
+ }) : !sharedConnectionId ? /*#__PURE__*/_jsx(TouchableOpacity, {
407
+ testID: `powerApps.connectionSetup.connector.${connectorId}.${existingConnected.length ? 'createButton' : 'connectButton'}`,
408
+ style: [styles.button, !requiredParametersComplete && styles.buttonDisabled],
409
+ disabled: !requiredParametersComplete,
219
410
  onPress: handleConnect,
220
411
  children: /*#__PURE__*/_jsx(Text, {
221
412
  style: styles.buttonText,
222
- children: "Connect"
413
+ children: existingConnected.length ? 'Create new' : metadata?.hasOAuthParameter ? 'Sign in' : 'Connect'
223
414
  })
224
- })
415
+ }) : null]
225
416
  }), (setup.phase === 'creating' || setup.phase === 'confirming') && /*#__PURE__*/_jsxs(View, {
417
+ testID: `powerApps.connectionSetup.connector.${connectorId}.${setup.phase}`,
226
418
  style: styles.statusRow,
227
419
  children: [/*#__PURE__*/_jsx(ActivityIndicator, {
228
420
  size: "small",
@@ -232,6 +424,7 @@ function ConnectorSetupStep({
232
424
  children: setup.phase === 'creating' ? 'Creating connection…' : 'Confirming…'
233
425
  })]
234
426
  }), setup.phase === 'oauth' && /*#__PURE__*/_jsxs(View, {
427
+ testID: `powerApps.connectionSetup.connector.${connectorId}.oauth`,
235
428
  style: styles.statusRow,
236
429
  children: [/*#__PURE__*/_jsx(ActivityIndicator, {
237
430
  size: "small",
@@ -240,30 +433,43 @@ function ConnectorSetupStep({
240
433
  style: styles.statusText,
241
434
  children: "Waiting for sign-in\u2026"
242
435
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
243
- onPress: handleWebViewDismiss,
436
+ testID: `powerApps.connectionSetup.connector.${connectorId}.doneButton`,
437
+ onPress: () => handleWebViewDismiss(),
244
438
  children: /*#__PURE__*/_jsx(Text, {
245
439
  style: styles.linkText,
246
440
  children: "Done"
247
441
  })
248
442
  })]
249
443
  }), setup.phase === 'connected' && /*#__PURE__*/_jsx(View, {
444
+ testID: `powerApps.connectionSetup.connector.${connectorId}.connected`,
250
445
  style: styles.statusRow,
251
446
  children: /*#__PURE__*/_jsx(Text, {
252
447
  style: styles.successText,
253
448
  children: "\u2713 Connected"
254
449
  })
255
450
  }), setup.phase === 'error' && /*#__PURE__*/_jsxs(View, {
451
+ testID: `powerApps.connectionSetup.connector.${connectorId}.error`,
256
452
  children: [/*#__PURE__*/_jsx(Text, {
257
453
  style: styles.errorText,
258
454
  children: setup.errorMessage ?? 'Connection failed'
259
455
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
456
+ testID: `powerApps.connectionSetup.connector.${connectorId}.retryButton`,
260
457
  style: [styles.button, styles.buttonDanger],
261
458
  onPress: setup.reset,
262
459
  children: /*#__PURE__*/_jsx(Text, {
263
460
  style: styles.buttonText,
264
461
  children: "Retry"
265
462
  })
266
- })]
463
+ }), onSignOut ? /*#__PURE__*/_jsx(TouchableOpacity, {
464
+ style: [styles.button, styles.buttonSecondary],
465
+ onPress: () => {
466
+ void onSignOut();
467
+ },
468
+ children: /*#__PURE__*/_jsx(Text, {
469
+ style: styles.buttonTextSecondary,
470
+ children: "Sign out"
471
+ })
472
+ }) : null]
267
473
  }), showOAuthModal && setup.consentUrl && renderOAuthWebView ? renderOAuthWebView(setup.consentUrl, handleWebViewDismiss) : null]
268
474
  });
269
475
  }
@@ -286,15 +492,31 @@ export function ConnectionSetupScreen({
286
492
  environmentId,
287
493
  region = 'prod',
288
494
  acquireToken,
495
+ onSignOut,
289
496
  isvClientId,
290
497
  onAllConnected,
291
498
  onSkip,
499
+ consentDetails = [],
500
+ resourceConsents = [],
501
+ onConsentDeclined,
502
+ showReview = true,
292
503
  renderOAuthWebView,
293
504
  title = 'Set up connections'
294
505
  }) {
295
506
  // resolvedIds accumulates as each connector is connected.
296
507
  const [resolvedIds, setResolvedIds] = useState({});
508
+ const [isSubmittingConsent, setIsSubmittingConsent] = useState(false);
509
+ const [consentError, setConsentError] = useState(null);
510
+ const [consentDeclined, setConsentDeclined] = useState(false);
511
+ const autoCompletionStartedRef = React.useRef(false);
297
512
  const insets = useSafeAreaInsets();
513
+ const missingReferencesKey = React.useMemo(() => missingConnectors.map(connector => `${connector.localRefId}:${normalizeApiId(connector.apiId)}`).sort().join('|'), [missingConnectors]);
514
+ useEffect(() => {
515
+ setResolvedIds({});
516
+ setConsentError(null);
517
+ setConsentDeclined(false);
518
+ autoCompletionStartedRef.current = false;
519
+ }, [missingReferencesKey]);
298
520
 
299
521
  // Build connectorId (short name) for each missing entry from the schemaMap.
300
522
  // schemaMap keys are data source names; we need to find the ConnectorSchema
@@ -310,26 +532,38 @@ export function ConnectionSetupScreen({
310
532
  }
311
533
  return map;
312
534
  }, [schemaMap]);
313
- const handleConnected = useCallback((apiId, instance) => {
535
+ const handleConnected = useCallback((localRefId, instance) => {
314
536
  setResolvedIds(prev => ({
315
537
  ...prev,
316
- [apiId]: instance.id
538
+ [localRefId]: instance.id
317
539
  }));
318
540
  }, []);
319
-
320
- // Fire onAllConnected once every connector has been resolved.
321
- // Done in an effect so it never runs inside a state updater (which would
322
- // trigger the "Cannot update a component while rendering" React error).
323
- useEffect(() => {
324
- if (missingConnectors.length === 0) return;
325
- const allResolved = missingConnectors.every(mc => resolvedIds[mc.apiId] !== undefined);
326
- if (allResolved) {
327
- onAllConnected(resolvedIds);
541
+ const allResolved = missingConnectors.every(mc => resolvedIds[mc.localRefId] !== undefined);
542
+ const handleAllow = useCallback(async () => {
543
+ if (!allResolved || isSubmittingConsent) return;
544
+ setIsSubmittingConsent(true);
545
+ setConsentError(null);
546
+ try {
547
+ await onAllConnected(resolvedIds);
548
+ } catch (error) {
549
+ setConsentError(error instanceof Error ? error.message : 'Unable to save connection consent.');
550
+ } finally {
551
+ setIsSubmittingConsent(false);
328
552
  }
329
- }, [resolvedIds, missingConnectors, onAllConnected]);
553
+ }, [allResolved, isSubmittingConsent, onAllConnected, resolvedIds]);
554
+ const handleDecline = useCallback(async () => {
555
+ setConsentDeclined(true);
556
+ await onConsentDeclined?.();
557
+ }, [onConsentDeclined]);
558
+ useEffect(() => {
559
+ if (showReview || !allResolved || autoCompletionStartedRef.current) return;
560
+ autoCompletionStartedRef.current = true;
561
+ void handleAllow();
562
+ }, [allResolved, handleAllow, showReview]);
330
563
  const resolvedCount = Object.keys(resolvedIds).length;
331
564
  const total = missingConnectors.length;
332
565
  return /*#__PURE__*/_jsxs(View, {
566
+ testID: "powerApps.connectionSetup.root",
333
567
  style: [styles.container, {
334
568
  paddingTop: insets.top,
335
569
  paddingBottom: insets.bottom
@@ -337,41 +571,121 @@ export function ConnectionSetupScreen({
337
571
  children: [/*#__PURE__*/_jsxs(ScrollView, {
338
572
  contentContainerStyle: styles.scrollContent,
339
573
  children: [/*#__PURE__*/_jsx(Text, {
574
+ testID: "powerApps.connectionSetup.title",
340
575
  style: styles.title,
341
576
  children: title
342
- }), /*#__PURE__*/_jsxs(Text, {
577
+ }), /*#__PURE__*/_jsx(Text, {
343
578
  style: styles.subtitle,
344
- children: ["This app needs ", total === 1 ? 'a connection' : `${total} connections`, " to work. Sign in to each service below to continue."]
579
+ children: total > 0 ? `This app needs ${total === 1 ? 'a connection' : `${total} connections`} to work. Sign in to each service below${showReview ? ', then review access' : ''}.` : 'Review the connections and resources this app will use.'
345
580
  }), resolvedCount > 0 && /*#__PURE__*/_jsxs(Text, {
581
+ testID: "powerApps.connectionSetup.progress",
346
582
  style: styles.progressText,
347
583
  children: [resolvedCount, " of ", total, " connected"]
348
584
  }), missingConnectors.map(mc => {
349
- const connectorId = connectorIdByApiId[mc.apiId];
585
+ const connectorId = mc.connectorId ?? connectorIdByApiId[normalizeApiId(mc.apiId)];
350
586
  if (!connectorId) {
351
587
  // Unknown connector — skip rendering.
352
588
  return null;
353
589
  }
354
- const isResolved = resolvedIds[mc.apiId] !== undefined;
590
+ const isResolved = resolvedIds[mc.localRefId] !== undefined;
355
591
  if (isResolved) {
356
592
  return /*#__PURE__*/_jsx(View, {
593
+ testID: `powerApps.connectionSetup.connector.${connectorId}.resolved`,
357
594
  style: [styles.connectorCard, styles.connectorCardResolved],
358
595
  children: /*#__PURE__*/_jsxs(Text, {
359
596
  style: styles.successText,
360
597
  children: ["\u2713 ", connectorId.replace('shared_', '')]
361
598
  })
362
- }, mc.apiId);
599
+ }, mc.localRefId);
363
600
  }
364
601
  return /*#__PURE__*/_jsx(ConnectorSetupStep, {
602
+ localRefId: mc.localRefId,
365
603
  connectorId: connectorId,
366
604
  apiId: mc.apiId,
367
605
  environmentId: environmentId,
368
606
  region: region,
369
607
  acquireToken: acquireToken,
608
+ onSignOut: onSignOut,
370
609
  isvClientId: isvClientId,
610
+ gatewayObjectIdHint: mc.gatewayObjectIdHint,
611
+ parameterSetName: mc.parameterSetName,
612
+ sharedConnectionId: mc.sharedConnectionId,
371
613
  onConnected: handleConnected,
372
614
  renderOAuthWebView: renderOAuthWebView
373
- }, mc.apiId);
374
- })]
615
+ }, mc.localRefId);
616
+ }), showReview && allResolved && !consentDeclined ? /*#__PURE__*/_jsxs(View, {
617
+ style: styles.consentSection,
618
+ children: [/*#__PURE__*/_jsx(Text, {
619
+ style: styles.consentTitle,
620
+ children: "Review access"
621
+ }), consentDetails.map(detail => /*#__PURE__*/_jsxs(View, {
622
+ style: styles.consentItem,
623
+ children: [/*#__PURE__*/_jsx(Text, {
624
+ style: styles.connectorName,
625
+ children: detail.displayName
626
+ }), detail.description ? /*#__PURE__*/_jsx(Text, {
627
+ style: styles.connectorDescription,
628
+ children: detail.description
629
+ }) : null, detail.appActions.length > 0 ? /*#__PURE__*/_jsxs(Text, {
630
+ style: styles.consentActions,
631
+ children: ["Uses: ", detail.appActions.join(', ')]
632
+ }) : null]
633
+ }, detail.localRefId)), resourceConsents.length > 0 ? /*#__PURE__*/_jsxs(View, {
634
+ style: styles.consentItem,
635
+ children: [/*#__PURE__*/_jsx(Text, {
636
+ style: styles.connectorName,
637
+ children: "Protected resources"
638
+ }), /*#__PURE__*/_jsx(Text, {
639
+ style: styles.connectorDescription,
640
+ children: resourceConsents.map(resource => resource.displayName).join(', ')
641
+ })]
642
+ }) : null, consentError ? /*#__PURE__*/_jsxs(View, {
643
+ children: [/*#__PURE__*/_jsx(Text, {
644
+ style: styles.errorText,
645
+ children: consentError
646
+ }), onSignOut ? /*#__PURE__*/_jsx(TouchableOpacity, {
647
+ style: [styles.button, styles.buttonSecondary],
648
+ onPress: () => {
649
+ void onSignOut();
650
+ },
651
+ children: /*#__PURE__*/_jsx(Text, {
652
+ style: styles.buttonTextSecondary,
653
+ children: "Sign out"
654
+ })
655
+ }) : null]
656
+ }) : null, /*#__PURE__*/_jsx(TouchableOpacity, {
657
+ style: [styles.button, isSubmittingConsent && styles.buttonDisabled],
658
+ disabled: isSubmittingConsent,
659
+ onPress: handleAllow,
660
+ children: isSubmittingConsent ? /*#__PURE__*/_jsx(ActivityIndicator, {
661
+ size: "small",
662
+ color: "#FFF"
663
+ }) : /*#__PURE__*/_jsx(Text, {
664
+ style: styles.buttonText,
665
+ children: "Allow"
666
+ })
667
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
668
+ style: styles.skipButton,
669
+ onPress: handleDecline,
670
+ children: /*#__PURE__*/_jsx(Text, {
671
+ style: styles.skipText,
672
+ children: "Do not allow"
673
+ })
674
+ })]
675
+ }) : null, showReview && consentDeclined ? /*#__PURE__*/_jsxs(View, {
676
+ style: styles.consentSection,
677
+ children: [/*#__PURE__*/_jsx(Text, {
678
+ style: styles.consentTitle,
679
+ children: "Access was not granted"
680
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
681
+ style: styles.button,
682
+ onPress: () => setConsentDeclined(false),
683
+ children: /*#__PURE__*/_jsx(Text, {
684
+ style: styles.buttonText,
685
+ children: "Review again"
686
+ })
687
+ })]
688
+ }) : null]
375
689
  }), onSkip ? /*#__PURE__*/_jsx(View, {
376
690
  style: styles.footer,
377
691
  children: /*#__PURE__*/_jsx(TouchableOpacity, {
@@ -467,6 +781,9 @@ const styles = StyleSheet.create({
467
781
  buttonDanger: {
468
782
  backgroundColor: '#C4262E'
469
783
  },
784
+ buttonDisabled: {
785
+ backgroundColor: '#A8A8A8'
786
+ },
470
787
  buttonText: {
471
788
  color: '#FFF',
472
789
  fontWeight: '600',
@@ -522,5 +839,67 @@ const styles = StyleSheet.create({
522
839
  alignItems: 'center',
523
840
  justifyContent: 'center',
524
841
  minHeight: 48
842
+ },
843
+ fieldGroup: {
844
+ marginTop: 12,
845
+ gap: 6
846
+ },
847
+ fieldLabel: {
848
+ color: '#333',
849
+ fontSize: 13,
850
+ fontWeight: '600'
851
+ },
852
+ textInput: {
853
+ minHeight: 42,
854
+ borderWidth: 1,
855
+ borderColor: '#C8C8C8',
856
+ borderRadius: 6,
857
+ paddingHorizontal: 10,
858
+ color: '#1A1A1A',
859
+ backgroundColor: '#FFF'
860
+ },
861
+ choiceRow: {
862
+ flexDirection: 'row',
863
+ flexWrap: 'wrap',
864
+ gap: 8
865
+ },
866
+ choiceButton: {
867
+ borderWidth: 1,
868
+ borderColor: '#C8C8C8',
869
+ borderRadius: 6,
870
+ paddingHorizontal: 10,
871
+ paddingVertical: 8
872
+ },
873
+ choiceButtonSelected: {
874
+ borderColor: '#0078D4',
875
+ backgroundColor: '#EFF6FC'
876
+ },
877
+ choiceText: {
878
+ color: '#333',
879
+ fontSize: 13
880
+ },
881
+ choiceTextSelected: {
882
+ color: '#0078D4',
883
+ fontSize: 13,
884
+ fontWeight: '600'
885
+ },
886
+ consentSection: {
887
+ marginTop: 12,
888
+ paddingTop: 16,
889
+ borderTopWidth: StyleSheet.hairlineWidth,
890
+ borderTopColor: '#D8D8D8'
891
+ },
892
+ consentTitle: {
893
+ fontSize: 18,
894
+ fontWeight: '700',
895
+ color: '#1A1A1A',
896
+ marginBottom: 12
897
+ },
898
+ consentItem: {
899
+ marginBottom: 12
900
+ },
901
+ consentActions: {
902
+ fontSize: 13,
903
+ color: '#444'
525
904
  }
526
905
  });