@controleonline/ui-common 1.2.71 → 1.2.76

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 (150) hide show
  1. package/.github/actions/workers/manager/action.yml +147 -0
  2. package/.github/actions/workers/qa/action.yml +44 -0
  3. package/.github/actions/workers/security/action.yml +44 -0
  4. package/.github/actions/workers/technical-documenter/action.yml +51 -0
  5. package/.github/workflows/manager-worker.yml +168 -0
  6. package/.github/workflows/publish-npm.yml +45 -0
  7. package/.gitmodules +3 -0
  8. package/AGENTS.md +6 -0
  9. package/README.md +14 -0
  10. package/package.json +3 -2
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +92 -22
  13. package/src/react/components/AddImportModal.js +94 -110
  14. package/src/react/components/BottomNavigationBar.config.js +211 -231
  15. package/src/react/components/CategoryForm.js +52 -35
  16. package/src/react/components/DefaultProvider.native.js +131 -95
  17. package/src/react/components/DefaultProvider.web.js +148 -74
  18. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  19. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  20. package/src/react/components/RuntimeInfoFooter.js +164 -104
  21. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  22. package/src/react/components/UnifiedPaymentBar.js +7 -4
  23. package/src/react/components/UserAvatar.js +160 -0
  24. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  25. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  26. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  27. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  28. package/src/react/config/deviceConfigBootstrap.js +5 -3
  29. package/src/react/pages/Connections.js +229 -0
  30. package/src/react/pages/Connections.styles.js +222 -0
  31. package/src/react/pages/DeviceDetailPage.js +2865 -0
  32. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  33. package/src/react/pages/Devices/currentDevice.js +194 -0
  34. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  35. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  36. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  37. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  38. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  39. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  40. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  41. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  42. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  43. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  44. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  45. package/src/react/pages/Devices.js +183 -0
  46. package/src/react/pages/Devices.styles.js +353 -0
  47. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  48. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  49. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  50. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  51. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  52. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  53. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  54. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  55. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  56. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  57. package/src/react/pages/Food99IntegrationPage.js +1 -0
  58. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  59. package/src/react/pages/GenericLogPage.js +42 -167
  60. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  61. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  62. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  63. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  64. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  65. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  66. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  67. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  68. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  69. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  70. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  71. package/src/react/pages/Imports.js +214 -166
  72. package/src/react/pages/IntegrationConfigPage.js +744 -0
  73. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  74. package/src/react/pages/Integrations.js +299 -0
  75. package/src/react/pages/Integrations.styles.js +130 -0
  76. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  77. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  78. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  79. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  80. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  81. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  82. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  83. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  84. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  85. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  86. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  87. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  88. package/src/react/pages/integrationsCatalog.js +399 -0
  89. package/src/react/services/Cielo/Cielo.js +193 -193
  90. package/src/react/utils/categoryContexts.js +46 -0
  91. package/src/react/utils/entityDisplay.js +222 -217
  92. package/src/react/utils/fileUrl.js +39 -20
  93. package/src/react/utils/integrationPage.js +16 -0
  94. package/src/react/utils/runtimeFooter.js +40 -22
  95. package/src/react/utils/runtimeLanguage.js +50 -9
  96. package/src/react/utils/runtimeMenu.js +191 -131
  97. package/src/react/utils/runtimeZoom.js +43 -0
  98. package/src/react/utils/shopConfig.js +27 -15
  99. package/src/react/utils/shopFranchises.js +126 -1
  100. package/src/react/utils/storeColumns.js +23 -4
  101. package/src/react/utils/userAvatar.js +104 -0
  102. package/src/store/categories/index.js +48 -20
  103. package/src/store/device_config/index.js +62 -21
  104. package/src/store/entity_log/index.js +22 -1
  105. package/src/store/imports/customActions.js +51 -0
  106. package/src/store/imports/index.js +89 -88
  107. package/src/store/status/index.js +11 -11
  108. package/src/store/themes/index.js +22 -0
  109. package/src/store/timezones/index.js +3 -1
  110. package/src/tests/browser/manager/connections.spec.js +143 -0
  111. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  112. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  113. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  114. package/src/tests/react/components/AddImportModal.test.js +166 -0
  115. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  116. package/src/tests/react/components/UserAvatar.test.js +52 -0
  117. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  118. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  119. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  120. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  121. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  122. package/src/tests/react/services/Cielo.test.js +190 -190
  123. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  124. package/src/tests/react/utils/appDomain.test.js +62 -0
  125. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  126. package/src/tests/react/utils/fileUrl.test.js +3 -3
  127. package/src/tests/react/utils/formatter.test.mjs +33 -0
  128. package/src/tests/react/utils/runtimeFooter.test.js +29 -4
  129. package/src/tests/react/utils/runtimeLanguage.test.js +79 -9
  130. package/src/tests/react/utils/runtimeMenu.test.js +198 -134
  131. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  132. package/src/tests/react/utils/shopConfig.test.js +10 -9
  133. package/src/tests/react/utils/shopFranchises.test.js +43 -0
  134. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  135. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  136. package/src/tests/react/utils/translate.test.mjs +141 -349
  137. package/src/tests/react/utils/userAvatar.test.js +63 -0
  138. package/src/utils/appDomain.js +15 -4
  139. package/src/utils/formatter.js +104 -16
  140. package/src/utils/integrationConfigs.js +80 -80
  141. package/src/utils/translate.js +222 -438
  142. package/src/utils/translateDiscovery.js +96 -0
  143. package/src/utils/translateNormalize.js +33 -0
  144. package/src/utils/translatePending.js +92 -0
  145. package/src/utils/translateStorage.js +142 -0
  146. package/.github/agents/developer.agent.md +0 -30
  147. package/.github/agents/devops.agent.md +0 -30
  148. package/.github/agents/qa.agent.md +0 -30
  149. package/.github/agents/security.agent.md +0 -30
  150. package/.github/workflows/npmjs.yml +0 -35
@@ -0,0 +1,744 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { ActivityIndicator, Linking, Platform, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import { useFocusEffect } from '@react-navigation/native';
5
+ import Icon from 'react-native-vector-icons/Feather';
6
+
7
+ import { api } from '@controleonline/ui-common/src/api';
8
+ import useToastMessage from '@controleonline/ui-crm/src/react/hooks/useToastMessage';
9
+ import { useStore } from '@store';
10
+ import { colors } from '@controleonline/../../src/styles/colors';
11
+ import {
12
+ resolveThemePalette,
13
+ withOpacity,
14
+ } from '@controleonline/../../src/styles/branding';
15
+
16
+ import {
17
+ getIntegrationConfig,
18
+ getIntegrationByKey,
19
+ parseIntegrationCollection,
20
+ } from './integrationsCatalog';
21
+ import DefaultUpload from '@controleonline/ui-default/src/react/components/upload/DefaultUpload';
22
+ import { extractFileId, toFileIri, uploadFileToApi } from '@controleonline/ui-default/src/react/components/upload/fileUpload';
23
+ import styles from './IntegrationConfigPage.styles';
24
+
25
+ const ROUTE_PROVIDER_MAP = {
26
+ UberIntegrationPage: 'uber',
27
+ AsaasIntegrationPage: 'asaas',
28
+ ClickSignIntegrationPage: 'clicksign',
29
+ };
30
+
31
+ const shadowStyle = Platform.select({
32
+ ios: {
33
+ shadowColor: '#0F172A',
34
+ shadowOffset: { width: 0, height: 8 },
35
+ shadowOpacity: 0.08,
36
+ shadowRadius: 16,
37
+ },
38
+ android: { elevation: 3 },
39
+ web: { boxShadow: '0 10px 24px rgba(15,23,42,0.08)' },
40
+ });
41
+
42
+ const routeNameToPath = routeName =>
43
+ String(routeName || '')
44
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
45
+ .toLowerCase();
46
+
47
+ const formatApiError = error => {
48
+ if (!error) return 'Nao foi possivel carregar a configuracao da integracao.';
49
+ if (typeof error === 'string') return error;
50
+ return error?.message || error?.description || error?.errmsg || 'Nao foi possivel carregar a configuracao da integracao.';
51
+ };
52
+
53
+ const normalizeSourceConfigs = source => {
54
+ if (Array.isArray(source)) {
55
+ return source.reduce((accumulator, item) => {
56
+ const key = String(item?.configKey || '').trim();
57
+ if (key) {
58
+ accumulator[key] = item?.configValue;
59
+ }
60
+ return accumulator;
61
+ }, {});
62
+ }
63
+
64
+ if (source && typeof source === 'object') {
65
+ return source;
66
+ }
67
+
68
+ return {};
69
+ };
70
+
71
+ const normalizeTextValue = value => {
72
+ let text = String(value ?? '').trim();
73
+ // Strip accidental surrounding quotes from double-encoded config values
74
+ if (
75
+ (text.startsWith('"') && text.endsWith('"') && text.length >= 2) ||
76
+ (text.startsWith("'") && text.endsWith("'") && text.length >= 2)
77
+ ) {
78
+ text = text.slice(1, -1).trim();
79
+ }
80
+ return text;
81
+ };
82
+
83
+ const isConnectedValue = value =>
84
+ value === true ||
85
+ value === 1 ||
86
+ value === '1' ||
87
+ String(value).trim().toLowerCase() === 'true';
88
+
89
+ const toConfigRequestValue = value => {
90
+ // Plain strings must NOT be JSON.stringify'd here — the HTTP JSON body
91
+ // already serializes them. Stringify caused values to be stored with
92
+ // literal surrounding quotes (e.g. ""whsec_...""), breaking webhook auth.
93
+ if (value === undefined || value === null) {
94
+ return '';
95
+ }
96
+
97
+ if (typeof value === 'string') {
98
+ return normalizeTextValue(value);
99
+ }
100
+
101
+ if (typeof value === 'object') {
102
+ return JSON.stringify(value);
103
+ }
104
+
105
+ return String(value);
106
+ };
107
+
108
+
109
+ const getConfigFields = providerConfig => {
110
+ if (!providerConfig) return [];
111
+ if (Array.isArray(providerConfig.tabs) && providerConfig.tabs.length > 0) {
112
+ return providerConfig.tabs.flatMap(tab => tab.fields || []);
113
+ }
114
+ return providerConfig.fields || [];
115
+ };
116
+
117
+ const buildFieldValues = (providerConfig, source) => {
118
+ const sourceMap = normalizeSourceConfigs(source);
119
+
120
+ return getConfigFields(providerConfig).reduce((accumulator, field) => {
121
+ accumulator[field.key] = normalizeTextValue(sourceMap[field.key]);
122
+ return accumulator;
123
+ }, {});
124
+ };
125
+
126
+ const extractAuthorizationUrl = response => {
127
+ const candidate =
128
+ response?.member?.[0]?.authorization_url ||
129
+ response?.member?.[0]?.auth_url ||
130
+ response?.member?.[0]?.url ||
131
+ response?.authorization_url ||
132
+ response?.auth_url ||
133
+ response?.url ||
134
+ response?.data?.authorization_url ||
135
+ response?.data?.auth_url ||
136
+ response?.data?.url;
137
+
138
+ return normalizeTextValue(candidate);
139
+ };
140
+
141
+ const formatUberOAuthError = error => {
142
+ const normalized = normalizeTextValue(error).toLowerCase();
143
+
144
+ if (normalized === 'invalid_scope') {
145
+ return 'O Uber nao liberou o scope pos_provisioning para este app. Esse app precisa estar aprovado/whitelisted no dashboard do Uber.';
146
+ }
147
+
148
+ if (normalized === 'access_denied') {
149
+ return 'O login do Uber foi cancelado.';
150
+ }
151
+
152
+ return normalizeTextValue(error) || 'Nao foi possivel concluir a conexao com o Uber.';
153
+ };
154
+
155
+ const openAuthorizationUrl = async authUrl => {
156
+ if (Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.location?.assign === 'function') {
157
+ window.location.assign(authUrl);
158
+ return;
159
+ }
160
+
161
+ await Linking.openURL(authUrl);
162
+ };
163
+
164
+ export default function IntegrationConfigPage({ route, navigation, embedded = false }) {
165
+ const peopleStore = useStore('people');
166
+ const themeStore = useStore('theme');
167
+ const configsStore = useStore('configs');
168
+ const { currentCompany } = peopleStore.getters;
169
+ const { colors: themeColors } = themeStore.getters;
170
+ const configActions = configsStore.actions;
171
+ const { isSaving } = configsStore.getters;
172
+ const { showError, showSuccess } = useToastMessage();
173
+ const oauthNoticeRef = useRef('');
174
+
175
+ const providerKey = useMemo(
176
+ () =>
177
+ route?.params?.providerKey ||
178
+ ROUTE_PROVIDER_MAP[route?.name] ||
179
+ '',
180
+ [route?.name, route?.params?.providerKey],
181
+ );
182
+ const providerConfig = useMemo(
183
+ () => getIntegrationConfig(providerKey),
184
+ [providerKey],
185
+ );
186
+ const configFields = useMemo(() => getConfigFields(providerConfig), [providerConfig]);
187
+ const fiscalTabs = providerConfig?.tabs || [];
188
+ const [activeFiscalTab, setActiveFiscalTab] = useState(
189
+ () => fiscalTabs[0]?.key || 'general',
190
+ );
191
+ const activeTabDef = useMemo(
192
+ () => fiscalTabs.find(tab => tab.key === activeFiscalTab) || fiscalTabs[0] || null,
193
+ [activeFiscalTab, fiscalTabs],
194
+ );
195
+ const visibleFields = useMemo(
196
+ () => (activeTabDef?.fields?.length ? activeTabDef.fields : configFields),
197
+ [activeTabDef, configFields],
198
+ );
199
+
200
+ useEffect(() => {
201
+ if (fiscalTabs.length && !fiscalTabs.some(tab => tab.key === activeFiscalTab)) {
202
+ setActiveFiscalTab(fiscalTabs[0].key);
203
+ }
204
+ }, [activeFiscalTab, fiscalTabs]);
205
+ const returnPath = useMemo(() => {
206
+ const routePath = normalizeTextValue(
207
+ route?.params?.return_path || route?.params?.returnPath || '',
208
+ );
209
+
210
+ if (routePath) {
211
+ return routePath.startsWith('/') ? routePath : `/${routePath}`;
212
+ }
213
+
214
+ const normalizedRoutePath = routeNameToPath(route?.name);
215
+ return normalizedRoutePath ? `/${normalizedRoutePath}` : '/uber-integration-page';
216
+ }, [route?.name, route?.params?.returnPath, route?.params?.return_path]);
217
+
218
+ const brandColors = useMemo(
219
+ () =>
220
+ resolveThemePalette(
221
+ {
222
+ ...themeColors,
223
+ ...(currentCompany?.theme?.colors || {}),
224
+ },
225
+ colors,
226
+ ),
227
+ [themeColors, currentCompany?.id],
228
+ );
229
+
230
+ const [loading, setLoading] = useState(true);
231
+ const [refreshing, setRefreshing] = useState(false);
232
+ const [authLoading, setAuthLoading] = useState(false);
233
+ const [configValues, setConfigValues] = useState({});
234
+ const [integrationSummary, setIntegrationSummary] = useState(null);
235
+
236
+ const providerId = currentCompany?.id;
237
+ const providerIri = useMemo(
238
+ () => (providerId ? `/people/${String(providerId).replace(/\D/g, '')}` : ''),
239
+ [providerId],
240
+ );
241
+
242
+ const syncConfigValues = useCallback(
243
+ source => {
244
+ if (!providerConfig) {
245
+ setConfigValues({});
246
+ return;
247
+ }
248
+
249
+ setConfigValues(buildFieldValues(providerConfig, source));
250
+ },
251
+ [providerConfig],
252
+ );
253
+
254
+ useEffect(() => {
255
+ syncConfigValues(currentCompany?.configs);
256
+ }, [currentCompany?.configs, syncConfigValues]);
257
+
258
+ useEffect(() => {
259
+ const oauthStatus = normalizeTextValue(route?.params?.oauth_status).toLowerCase();
260
+ const oauthError = normalizeTextValue(route?.params?.oauth_error);
261
+ const oauthKey = `${oauthStatus}|${oauthError}`;
262
+
263
+ if (!oauthStatus || oauthNoticeRef.current === oauthKey) {
264
+ return;
265
+ }
266
+
267
+ oauthNoticeRef.current = oauthKey;
268
+
269
+ if (oauthStatus === 'success') {
270
+ showSuccess('Uber conectado com sucesso.');
271
+ return;
272
+ }
273
+
274
+ if (oauthStatus === 'error') {
275
+ showError(formatUberOAuthError(oauthError));
276
+ }
277
+ }, [route?.params?.oauth_error, route?.params?.oauth_status, showError, showSuccess]);
278
+
279
+ const loadPageData = useCallback(async ({ showLoading = true } = {}) => {
280
+ if (!providerIri || !providerConfig) {
281
+ setIntegrationSummary(null);
282
+ setLoading(false);
283
+ return;
284
+ }
285
+
286
+ if (showLoading) {
287
+ setLoading(true);
288
+ }
289
+
290
+ try {
291
+ const integrationPromise = api.fetch('/marketplace/integrations', {
292
+ params: {
293
+ provider_id: providerId,
294
+ },
295
+ });
296
+
297
+ if (configFields.length > 0) {
298
+ const [configResponse, integrationResponse] = await Promise.all([
299
+ api.fetch('/configs', {
300
+ params: {
301
+ people: providerIri,
302
+ },
303
+ }),
304
+ integrationPromise,
305
+ ]);
306
+
307
+ syncConfigValues(parseIntegrationCollection(configResponse));
308
+ setIntegrationSummary(
309
+ getIntegrationByKey(integrationResponse, providerConfig.key),
310
+ );
311
+ return;
312
+ }
313
+
314
+ const integrationResponse = await integrationPromise;
315
+ setIntegrationSummary(
316
+ getIntegrationByKey(integrationResponse, providerConfig.key),
317
+ );
318
+ } catch (error) {
319
+ showError(formatApiError(error));
320
+ syncConfigValues(currentCompany?.configs);
321
+ setIntegrationSummary(null);
322
+ } finally {
323
+ if (showLoading) {
324
+ setLoading(false);
325
+ }
326
+ }
327
+ }, [
328
+ configFields.length,
329
+ currentCompany?.configs,
330
+ providerConfig,
331
+ providerIri,
332
+ providerId,
333
+ showError,
334
+ syncConfigValues,
335
+ ]);
336
+
337
+ useFocusEffect(
338
+ useCallback(() => {
339
+ loadPageData();
340
+ }, [loadPageData]),
341
+ );
342
+
343
+ const onRefresh = useCallback(async () => {
344
+ setRefreshing(true);
345
+ try {
346
+ await loadPageData({ showLoading: false });
347
+ } finally {
348
+ setRefreshing(false);
349
+ }
350
+ }, [loadPageData]);
351
+
352
+ const updateField = useCallback((fieldKey, value) => {
353
+ setConfigValues(currentValues => ({
354
+ ...currentValues,
355
+ [fieldKey]: value,
356
+ }));
357
+ }, []);
358
+
359
+ const handleOAuthConnect = useCallback(async () => {
360
+ if (!providerIri || !providerConfig || !providerConfig.oauthConnect) {
361
+ showError('Nao foi possivel identificar a integracao selecionada.');
362
+ return;
363
+ }
364
+
365
+ setAuthLoading(true);
366
+ try {
367
+ const response = await api.fetch(providerConfig.authorizationEndpoint, {
368
+ method: 'POST',
369
+ body: {
370
+ provider_id: providerId,
371
+ return_path: returnPath,
372
+ },
373
+ });
374
+
375
+ const authUrl = extractAuthorizationUrl(response);
376
+ if (!authUrl) {
377
+ showError('Nao foi possivel iniciar o login do Uber.');
378
+ return;
379
+ }
380
+
381
+ await openAuthorizationUrl(authUrl);
382
+ showSuccess('Abrindo login do Uber.');
383
+ } catch (error) {
384
+ showError(error?.message || 'Nao foi possivel iniciar o login do Uber.');
385
+ } finally {
386
+ setAuthLoading(false);
387
+ }
388
+ }, [providerConfig, providerId, providerIri, returnPath, showError, showSuccess]);
389
+
390
+ const requiredKeys = providerConfig?.requiredKeys || [];
391
+ const connected = integrationSummary && typeof integrationSummary.connected !== 'undefined'
392
+ ? isConnectedValue(integrationSummary.connected)
393
+ : requiredKeys.length > 0
394
+ ? requiredKeys.every(fieldKey => normalizeTextValue(configValues[fieldKey]) !== '')
395
+ : false;
396
+ const statusTone = connected ? '#16A34A' : '#e67e22';
397
+ const statusText = connected ? 'Conectado' : 'Pendente';
398
+ const editable = Boolean(providerIri && providerConfig && !isSaving && !loading && !providerConfig.oauthConnect);
399
+ const actionLoading = providerConfig?.oauthConnect ? authLoading : isSaving;
400
+ const actionDisabled = providerConfig?.oauthConnect ? authLoading || loading : !editable;
401
+
402
+ const saveIntegration = useCallback(async () => {
403
+ if (!providerIri || !providerConfig || providerConfig.oauthConnect) {
404
+ showError('Nao foi possivel identificar a integracao selecionada.');
405
+ return;
406
+ }
407
+
408
+ const configs = configFields.map(field => ({
409
+ configKey: field.key,
410
+ configValue: toConfigRequestValue(normalizeTextValue(configValues[field.key])),
411
+ }));
412
+
413
+ try {
414
+ await configActions.addManyConfigs({
415
+ configs,
416
+ people: providerIri,
417
+ module: 4,
418
+ visibility: 'public',
419
+ });
420
+
421
+ showSuccess(`${providerConfig.label} salvo com sucesso.`);
422
+ await loadPageData({ showLoading: false });
423
+ } catch (error) {
424
+ showError(error?.message || 'Nao foi possivel salvar a integracao.');
425
+ }
426
+ }, [
427
+ configActions,
428
+ configFields,
429
+ configValues,
430
+ loadPageData,
431
+ providerConfig,
432
+ providerIri,
433
+ showError,
434
+ showSuccess,
435
+ ]);
436
+
437
+ if (!providerConfig) {
438
+ return (
439
+ <SafeAreaView style={styles.container} edges={['bottom']}>
440
+ <View style={styles.centerState}>
441
+ <Icon name="alert-triangle" size={32} color="#e67e22" />
442
+ <Text style={styles.centerStateTitle}>Integracao indisponivel</Text>
443
+ <Text style={styles.centerStateText}>
444
+ A tela solicitada nao possui configuracao cadastrada.
445
+ </Text>
446
+ </View>
447
+ </SafeAreaView>
448
+ );
449
+ }
450
+
451
+ if (!providerId) {
452
+ return (
453
+ <SafeAreaView style={styles.container} edges={['bottom']}>
454
+ <View style={styles.centerState}>
455
+ <Icon name="building" size={32} color="#94A3B8" />
456
+ <Text style={styles.centerStateTitle}>Selecione uma empresa</Text>
457
+ <Text style={styles.centerStateText}>
458
+ A configuracao da integracao depende da empresa ativa.
459
+ </Text>
460
+ </View>
461
+ </SafeAreaView>
462
+ );
463
+ }
464
+
465
+ if (loading) {
466
+ return (
467
+ <SafeAreaView style={[styles.container, { backgroundColor: brandColors.background }]} edges={['bottom']}>
468
+ <View style={styles.centerState}>
469
+ <ActivityIndicator size="large" color={providerConfig.accent} />
470
+ <Text style={styles.centerStateTitle}>Carregando integracao</Text>
471
+ <Text style={styles.centerStateText}>
472
+ Buscando as credenciais salvas para a empresa ativa.
473
+ </Text>
474
+ </View>
475
+ </SafeAreaView>
476
+ );
477
+ }
478
+
479
+ return (
480
+ <SafeAreaView style={[styles.container, { backgroundColor: brandColors.background }]} edges={['bottom']}>
481
+ <ScrollView
482
+ contentContainerStyle={styles.scroll}
483
+ showsVerticalScrollIndicator={false}
484
+ refreshControl={
485
+ <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={providerConfig.accent} />
486
+ }>
487
+ {!embedded ? (
488
+ <View style={[styles.heroCard, shadowStyle, { backgroundColor: providerConfig.accent }]}>
489
+ <View style={styles.heroCopy}>
490
+ <Text style={styles.heroEyebrow}>INTEGRACAO</Text>
491
+ <Text style={styles.heroTitle}>{providerConfig.label}</Text>
492
+ <Text style={styles.heroText}>
493
+ {providerConfig.description}
494
+ </Text>
495
+ </View>
496
+ <View style={styles.heroBadge}>
497
+ <Icon name={providerConfig.icon} size={22} color={providerConfig.accent} />
498
+ </View>
499
+ </View>
500
+ ) : (
501
+ <View style={[styles.embeddedHeader, shadowStyle]}>
502
+ <Text style={styles.embeddedTitle}>Configuracoes fiscais</Text>
503
+ </View>
504
+ )}
505
+
506
+ {!embedded ? (
507
+ <View style={[styles.statusCard, shadowStyle]}>
508
+ <View style={styles.statusHeader}>
509
+ <View style={styles.statusCopy}>
510
+ <Text style={styles.sectionTitle}>Status</Text>
511
+ <Text style={styles.sectionSubtitle}>
512
+ {providerConfig.oauthConnect
513
+ ? 'A integracao fica conectada quando o login do Uber termina e o store e salvo automaticamente.'
514
+ : 'A integracao so aparece como conectada quando todos os campos obrigatorios foram salvos na empresa ativa.'}
515
+ </Text>
516
+ </View>
517
+
518
+ <View
519
+ style={[
520
+ styles.statusBadge,
521
+ { backgroundColor: withOpacity(statusTone, 0.12) },
522
+ ]}>
523
+ <Text style={[styles.statusBadgeText, { color: statusTone }]}>
524
+ {statusText}
525
+ </Text>
526
+ </View>
527
+ </View>
528
+ </View>
529
+ ) : null}
530
+
531
+ <View style={[styles.formCard, shadowStyle]}>
532
+ <Text style={styles.cardTitle}>
533
+ {providerConfig.oauthConnect
534
+ ? 'Conexao'
535
+ : fiscalTabs.length
536
+ ? (activeTabDef?.label || 'Configuracoes')
537
+ : 'Credenciais'}
538
+ </Text>
539
+ {!embedded ? (
540
+ <Text style={styles.cardSubtitle}>
541
+ {providerConfig.oauthConnect
542
+ ? 'Use o login oficial do Uber. A store sera localizada e gravada automaticamente na empresa ativa.'
543
+ : 'Salve as credenciais na empresa ativa. O hub de integracoes volta a mostrar o status correto quando voce retornar para a lista.'}
544
+ </Text>
545
+ ) : null}
546
+
547
+ {providerConfig.oauthConnect ? (
548
+ <View style={styles.fieldList}>
549
+ <View style={styles.fieldGroup}>
550
+ <Text style={styles.fieldLabel}>Uber OAuth</Text>
551
+ <Text style={styles.fieldKey}>
552
+ Nao ha campos manuais. O login autoriza o app e salva o store automaticamente.
553
+ </Text>
554
+ </View>
555
+ </View>
556
+ ) : (
557
+ <View style={styles.fieldList}>
558
+ {fiscalTabs.length > 0 ? (
559
+ <View style={styles.subTabRow}>
560
+ {fiscalTabs.map(tab => {
561
+ const selected = tab.key === (activeTabDef?.key || activeFiscalTab);
562
+ return (
563
+ <TouchableOpacity
564
+ key={tab.key}
565
+ style={[styles.subTabButton, selected && styles.subTabButtonActive]}
566
+ activeOpacity={0.85}
567
+ onPress={() => setActiveFiscalTab(tab.key)}>
568
+ <Text
569
+ style={[
570
+ styles.subTabLabel,
571
+ selected && styles.subTabLabelActive,
572
+ ]}>
573
+ {tab.label}
574
+ </Text>
575
+ </TouchableOpacity>
576
+ );
577
+ })}
578
+ </View>
579
+ ) : null}
580
+ {activeTabDef?.description ? (
581
+ <Text style={styles.tabDescription}>{activeTabDef.description}</Text>
582
+ ) : null}
583
+ {visibleFields.map(field => (
584
+ <View key={field.key} style={styles.fieldGroup}>
585
+ <Text style={styles.fieldLabel}>{field.label}</Text>
586
+ {!embedded ? <Text style={styles.fieldKey}>{field.key}</Text> : null}
587
+ {field.type === 'select' ? (
588
+ <View style={styles.selectList}>
589
+ {(field.options || []).map(option => {
590
+ const selected = String(configValues[field.key] || '') === String(option.value);
591
+ return (
592
+ <TouchableOpacity
593
+ key={String(option.value)}
594
+ style={[
595
+ styles.selectOption,
596
+ selected && styles.selectOptionActive,
597
+ !editable && styles.inputDisabled,
598
+ ]}
599
+ disabled={!editable}
600
+ activeOpacity={0.85}
601
+ onPress={() => updateField(field.key, String(option.value))}>
602
+ <Text
603
+ style={[
604
+ styles.selectOptionText,
605
+ selected && styles.selectOptionTextActive,
606
+ ]}>
607
+ {option.label}
608
+ </Text>
609
+ </TouchableOpacity>
610
+ );
611
+ })}
612
+ </View>
613
+ ) : field.type === 'file' ? (
614
+ <View style={styles.fileFieldWrap}>
615
+ {configValues[field.key] ? (
616
+ <Text style={styles.fieldHint}>
617
+ Arquivo vinculado (id: {String(configValues[field.key]).replace(/\D/g, '') || configValues[field.key]})
618
+ </Text>
619
+ ) : (
620
+ <Text style={styles.fieldHint}>Nenhum certificado vinculado.</Text>
621
+ )}
622
+ <DefaultUpload
623
+ relationStoreName="people"
624
+ relationField="people"
625
+ relationResource="people"
626
+ entityId={providerId}
627
+ companyId={providerId}
628
+ context={field.fileContext || 'company_certificate'}
629
+ libraryContexts={[field.fileContext || 'company_certificate']}
630
+ acceptedTypes={field.accept || '.pfx,.p12,application/x-pkcs12'}
631
+ fileType=""
632
+ fileTypeLabel="certificado"
633
+ title={field.label}
634
+ triggerLabel="Gerenciar certificado"
635
+ managerTitle="Gerenciador de arquivos"
636
+ searchPlaceholder="Buscar certificado"
637
+ uploadButtonLabel="Enviar certificado"
638
+ emptyAttachmentLabel="Nenhum certificado anexado."
639
+ emptyLibraryLabel="Nenhum arquivo encontrado."
640
+ uploadSuccessMessage="Certificado enviado."
641
+ attachSuccessMessage="Certificado vinculado."
642
+ removeSuccessMessage="Certificado removido."
643
+ showInlineContent={false}
644
+ uploadResultAlreadyAttached
645
+ requireEntity={false}
646
+ onUploadFile={async ({ file, companyId, context, entityId }) => {
647
+ const uploaded = await uploadFileToApi({
648
+ file,
649
+ context: context || field.fileContext || 'company_certificate',
650
+ peopleId: companyId || providerId,
651
+ entityId: entityId || providerId,
652
+ });
653
+ const id = extractFileId(uploaded);
654
+ const iri = toFileIri(uploaded);
655
+ const value = id ? String(id) : iri || '';
656
+ if (!value) {
657
+ throw new Error('Upload sem identificador de arquivo.');
658
+ }
659
+ updateField(field.key, value);
660
+ return uploaded;
661
+ }}
662
+ onAttachFile={async fileObj => {
663
+ const id = extractFileId(fileObj);
664
+ const iri = toFileIri(fileObj);
665
+ const value = id ? String(id) : iri || '';
666
+ if (!value) {
667
+ throw new Error('Arquivo sem identificador.');
668
+ }
669
+ updateField(field.key, value);
670
+ return fileObj;
671
+ }}
672
+ onRemoveAttachment={async () => {
673
+ updateField(field.key, '');
674
+ return true;
675
+ }}
676
+ renderTrigger={({ openManager, uploading }) => (
677
+ <TouchableOpacity
678
+ style={[
679
+ styles.filePickerButton,
680
+ !editable && styles.inputDisabled,
681
+ ]}
682
+ disabled={!editable || uploading}
683
+ activeOpacity={0.85}
684
+ onPress={openManager}>
685
+ {uploading ? (
686
+ <ActivityIndicator color="#166534" />
687
+ ) : (
688
+ <Icon name="folder" size={16} color="#166534" />
689
+ )}
690
+ <Text style={styles.filePickerButtonText}>
691
+ {configValues[field.key]
692
+ ? 'Trocar certificado (gerenciador)'
693
+ : 'Selecionar / enviar certificado'}
694
+ </Text>
695
+ </TouchableOpacity>
696
+ )}
697
+ />
698
+ </View>
699
+ ) : (
700
+ <TextInput
701
+ style={[
702
+ styles.input,
703
+ !editable && styles.inputDisabled,
704
+ ]}
705
+ value={configValues[field.key] || ''}
706
+ onChangeText={value => updateField(field.key, value)}
707
+ editable={editable}
708
+ autoCapitalize="none"
709
+ autoCorrect={false}
710
+ secureTextEntry={Boolean(field.secureTextEntry)}
711
+ placeholder={field.placeholder}
712
+ />
713
+ )}
714
+ </View>
715
+ ))}
716
+ </View>
717
+ )}
718
+
719
+ <TouchableOpacity
720
+ style={[
721
+ styles.saveButton,
722
+ { backgroundColor: providerConfig.accent },
723
+ actionDisabled && styles.saveButtonDisabled,
724
+ ]}
725
+ disabled={actionDisabled}
726
+ activeOpacity={0.9}
727
+ onPress={providerConfig.oauthConnect ? handleOAuthConnect : saveIntegration}>
728
+ {actionLoading ? (
729
+ <ActivityIndicator color="#FFFFFF" />
730
+ ) : (
731
+ <Icon name={providerConfig.oauthConnect ? 'log-in' : 'save'} size={16} color="#FFFFFF" />
732
+ )}
733
+ <Text style={styles.saveButtonText}>
734
+ {providerConfig.oauthConnect
735
+ ? providerConfig.connectLabel || 'Conectar'
736
+ : providerConfig.saveLabel}
737
+ </Text>
738
+ </TouchableOpacity>
739
+ </View>
740
+ </ScrollView>
741
+ </SafeAreaView>
742
+ );
743
+ }
744
+ // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores, remover api.fetch e evitar repassar dados em objetos quando o store ja resolver isso.