@controleonline/ui-common 1.2.70 → 1.2.71
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/package.json +31 -28
- package/src/api/index.js +237 -37
- package/src/react/components/AddImportModal.js +1 -1
- package/src/react/components/AnimatedModal.js +171 -0
- package/src/react/components/AnimatedModal.styles.js +21 -0
- package/src/react/components/AppTypeSwitcher.js +164 -0
- package/src/react/components/AppTypeSwitcher.styles.js +109 -0
- package/src/react/components/BackgroundRuntimeBridge.js +5 -4
- package/src/react/components/BottomNavigationBar.js +86 -31
- package/src/react/components/BottomNavigationBar.styles.js +118 -110
- package/src/react/components/DefaultProvider.native.js +68 -66
- package/src/react/components/DefaultProvider.web.js +44 -42
- package/src/react/components/DeliveryPushBridge.native.js +2 -2
- package/src/react/components/DeviceAlertSoundService.js +3 -3
- package/src/react/components/KioskModeBridge.js +2 -2
- package/src/react/components/LauncherModeBridge.js +2 -2
- package/src/react/components/ManagerPushBridge.native.js +2 -2
- package/src/react/components/RemoteCheckoutService.js +28 -20
- package/src/react/components/RuntimeBottomNavigationBar.js +146 -119
- package/src/react/components/RuntimeInfoFooter.js +37 -9
- package/src/react/components/StateStore.js +258 -0
- package/src/react/components/WebsocketListener.native.js +11 -11
- package/src/react/config/deviceConfigBootstrap.js +66 -27
- package/src/react/css/orders.js +72 -0
- package/src/react/pages/SettingsPage/PaymentTypesByWalletTab.js +484 -454
- package/src/react/pages/SettingsPage/index.js +1266 -1167
- package/src/react/print/providers/local.js +1 -1
- package/src/react/router/publicRoutes.js +25 -0
- package/src/react/services/Cielo/Checkout.js +39 -0
- package/src/react/services/Cielo/Cielo.js +193 -0
- package/src/react/services/Cielo/Print.js +7 -0
- package/src/react/services/InfinitePay/Checkout.js +33 -0
- package/src/react/services/InfinitePay/InfinitePay.js +24 -0
- package/src/react/{utils → services}/paymentGatewayExecution.js +91 -91
- package/src/react/styles/global.js +115 -0
- package/src/react/utils/fileUrl.js +182 -16
- package/src/react/utils/menuNavigation.js +31 -0
- package/src/react/utils/orderIdentity.js +277 -0
- package/src/react/utils/remotePayment.js +115 -61
- package/src/react/utils/runtimeMenu.js +35 -3
- package/src/react/utils/shopConfig.js +50 -24
- package/src/react/utils/shopFranchises.js +56 -22
- package/src/react/utils/socketRuntimePipeline.js +14 -14
- package/src/store/configs/index.js +2 -2
- package/src/tests/react/api/loadSmokeIndex.test.js +75 -0
- package/src/tests/react/config/deviceConfigBootstrap.test.js +47 -0
- package/src/tests/react/print/localPrintProvider.test.js +1 -1
- package/src/tests/react/services/Cielo.test.js +190 -0
- package/src/tests/react/utils/fileUrl.test.js +62 -0
- package/src/tests/react/utils/importStatus.test.js +2 -2
- package/src/tests/react/utils/orderIdentity.test.js +139 -0
- package/src/tests/react/utils/remotePayment.test.js +62 -0
- package/src/tests/react/utils/runtimeFooter.test.js +4 -6
- package/src/tests/react/utils/runtimeLanguage.test.js +2 -2
- package/src/tests/react/utils/runtimeMenu.test.js +23 -3
- package/src/tests/react/utils/shopConfig.test.js +74 -0
- package/src/tests/react/utils/shopFranchises.test.js +79 -12
- package/src/tests/react/utils/translate.test.mjs +166 -102
- package/src/utils/formatter.js +18 -0
- package/src/utils/integrationConfigs.js +80 -79
- package/src/utils/translate.js +125 -30
package/package.json
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@controleonline/ui-common",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"author": "ControleOnline",
|
|
5
|
-
"description": "ControleOnline Common Quasar UI Component",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"main": "src/react/index.js",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"test": "npm run test:imports-status && npm run test:runtime-language && npm run test:runtime-footer && npm run test:invoice-presentation",
|
|
10
|
-
"test:imports-status": "mkdir -p node_modules/@controleonline && ln -sfn ../.. node_modules/@controleonline/ui-common && node --test src/tests/react/utils/importStatus.test.js",
|
|
11
|
-
"test:runtime-language": "node --test src/tests/react/utils/runtimeLanguage.test.js",
|
|
12
|
-
"test:runtime-footer": "node --test src/tests/react/utils/runtimeFooter.test.js",
|
|
13
|
-
"test:invoice-presentation": "node --test src/tests/react/utils/invoicePresentation.test.mjs",
|
|
14
|
-
"test:coverage": "c8 --reporter=clover --report-dir coverage npm test"
|
|
15
|
-
},
|
|
16
|
-
"devDependencies": {
|
|
17
|
-
"c8": "^10.1.3"
|
|
18
|
-
},
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "https://github.com/ControleOnline/ui-common.git"
|
|
22
|
-
},
|
|
23
|
-
"bugs": "",
|
|
24
|
-
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
25
|
-
"browserslist": [
|
|
26
|
-
"last 1 version, not dead, ie >= 11"
|
|
27
|
-
]
|
|
28
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@controleonline/ui-common",
|
|
3
|
+
"version": "1.2.71",
|
|
4
|
+
"author": "ControleOnline",
|
|
5
|
+
"description": "ControleOnline Common Quasar UI Component",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/react/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "npm run test:imports-status && npm run test:runtime-language && npm run test:runtime-footer && npm run test:invoice-presentation",
|
|
10
|
+
"test:imports-status": "mkdir -p node_modules/@controleonline && ln -sfn ../.. node_modules/@controleonline/ui-common && node --test src/tests/react/utils/importStatus.test.js",
|
|
11
|
+
"test:runtime-language": "node --test src/tests/react/utils/runtimeLanguage.test.js",
|
|
12
|
+
"test:runtime-footer": "node --test src/tests/react/utils/runtimeFooter.test.js",
|
|
13
|
+
"test:invoice-presentation": "node --test src/tests/react/utils/invoicePresentation.test.mjs",
|
|
14
|
+
"test:coverage": "c8 --reporter=clover --report-dir coverage npm test"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"c8": "^10.1.3"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/ControleOnline/ui-common.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": "",
|
|
24
|
+
"homepage": "https://www.controleonline.com/devs/plugins",
|
|
25
|
+
"browserslist": [
|
|
26
|
+
"last 1 version, not dead, ie >= 11"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@controleonline/ui-default": "*"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/api/index.js
CHANGED
|
@@ -3,9 +3,177 @@ import axios from 'axios';
|
|
|
3
3
|
import { APP_ENV } from '../../../../../config/env';
|
|
4
4
|
import { resolveAppDomain } from '@controleonline/ui-common/src/utils/appDomain';
|
|
5
5
|
import { resolveApiEntryPoint } from '@controleonline/ui-common/src/utils/apiEntryPoint';
|
|
6
|
-
|
|
7
|
-
const MIME_TYPE = 'application/ld+json';
|
|
8
|
-
|
|
6
|
+
|
|
7
|
+
const MIME_TYPE = 'application/ld+json';
|
|
8
|
+
|
|
9
|
+
function encodeBasicAuth(user, password) {
|
|
10
|
+
const credentials = `${String(user || '').trim()}:${String(password || '').trim()}`;
|
|
11
|
+
|
|
12
|
+
if (credentials === ':') {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof btoa === 'function') {
|
|
17
|
+
return btoa(credentials);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof Buffer !== 'undefined') {
|
|
21
|
+
return Buffer.from(credentials, 'utf8').toString('base64');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveApiRuntimeConfig(config = {}) {
|
|
28
|
+
return {
|
|
29
|
+
apiBaseUrl: String(config.apiBaseUrl || APP_ENV.API_ENTRYPOINT || '').trim(),
|
|
30
|
+
domain: String(config.domain || APP_ENV.DOMAIN || '').trim(),
|
|
31
|
+
htaccessUser: String(config.htaccessUser || APP_ENV.HTACCESS_USER || '').trim(),
|
|
32
|
+
htaccessPassword: String(config.htaccessPassword || APP_ENV.HTACCESS_PASSWORD || '').trim(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildApiUrl(apiBaseUrl, path) {
|
|
37
|
+
const apiEntryPoint = resolveApiEntryPoint(apiBaseUrl);
|
|
38
|
+
const entryPoint = apiEntryPoint + (apiEntryPoint.endsWith('/') ? '' : '/');
|
|
39
|
+
const normalizedPath = String(path || '').startsWith('/')
|
|
40
|
+
? String(path || '').substring(1)
|
|
41
|
+
: String(path || '');
|
|
42
|
+
|
|
43
|
+
return new URL(normalizedPath, entryPoint).href;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readSmokeResponseBody(response, responseType) {
|
|
47
|
+
if (responseType === 'blob') {
|
|
48
|
+
return await response.blob();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (responseType === 'text') {
|
|
52
|
+
return await response.text();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
|
|
57
|
+
if (!text) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSmokeHttpError(response, body) {
|
|
69
|
+
const message =
|
|
70
|
+
String(body?.message || body?.error || body || response.statusText || 'Request failed')
|
|
71
|
+
.trim() || 'Request failed';
|
|
72
|
+
|
|
73
|
+
const error = new Error(message);
|
|
74
|
+
error.code = response.status;
|
|
75
|
+
error.status = response.status;
|
|
76
|
+
error.body = body;
|
|
77
|
+
return error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function requestSmoke(path, options = {}, config = {}) {
|
|
81
|
+
const runtimeConfig = resolveApiRuntimeConfig(config);
|
|
82
|
+
const url = buildApiUrl(runtimeConfig.apiBaseUrl, path);
|
|
83
|
+
const headers = new Headers(options.headers || undefined);
|
|
84
|
+
const accept =
|
|
85
|
+
options.responseType === 'blob' || options.responseType === 'text'
|
|
86
|
+
? '*/*'
|
|
87
|
+
: MIME_TYPE;
|
|
88
|
+
|
|
89
|
+
if (accept !== '' && !headers.has('Accept')) {
|
|
90
|
+
headers.set('Accept', accept);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (options.responseType !== 'blob' && !headers.has('Content-Type')) {
|
|
94
|
+
headers.set('Content-Type', MIME_TYPE);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sessionToken = await api.getToken();
|
|
98
|
+
|
|
99
|
+
if (sessionToken && !headers.has('API-TOKEN')) {
|
|
100
|
+
headers.set('API-TOKEN', sessionToken);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const basicAuth = encodeBasicAuth(runtimeConfig.htaccessUser, runtimeConfig.htaccessPassword);
|
|
104
|
+
if (basicAuth !== '' && !headers.has('Authorization')) {
|
|
105
|
+
headers.set('Authorization', `Basic ${basicAuth}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const appDomain = resolveAppDomain(runtimeConfig.domain);
|
|
109
|
+
if (appDomain !== '' && !headers.has('App-Domain')) {
|
|
110
|
+
headers.set('App-Domain', appDomain);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let body = options.body;
|
|
114
|
+
const hasBody =
|
|
115
|
+
body !== undefined &&
|
|
116
|
+
body !== null &&
|
|
117
|
+
options.method !== 'GET' &&
|
|
118
|
+
options.method !== 'HEAD';
|
|
119
|
+
|
|
120
|
+
if (hasBody && typeof body !== 'string' && !(body instanceof FormData) && !(body instanceof Blob)) {
|
|
121
|
+
body = JSON.stringify(body);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.params) {
|
|
125
|
+
const params = api.serialize(options.params);
|
|
126
|
+
if (params.length > 0) {
|
|
127
|
+
const urlObject = new URL(url);
|
|
128
|
+
urlObject.search = params.join('&');
|
|
129
|
+
return requestSmokeUrl(urlObject.toString(), {
|
|
130
|
+
...options,
|
|
131
|
+
body,
|
|
132
|
+
headers,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return requestSmokeUrl(url, {
|
|
138
|
+
...options,
|
|
139
|
+
body,
|
|
140
|
+
headers,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function requestSmokeUrl(url, options) {
|
|
145
|
+
const response = await fetch(url, options);
|
|
146
|
+
|
|
147
|
+
if (options.responseType === 'blob') {
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
throw buildSmokeHttpError(response, await response.text().catch(() => ''));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return await response.blob();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const responseBody = await readSmokeResponseBody(response, options.responseType);
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw buildSmokeHttpError(response, responseBody);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
responseBody &&
|
|
163
|
+
typeof responseBody === 'object' &&
|
|
164
|
+
(responseBody['@type'] === 'Error' ||
|
|
165
|
+
responseBody['@type'] === 'ConstraintViolationList')
|
|
166
|
+
) {
|
|
167
|
+
throw buildSmokeHttpError(response, responseBody);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (options.responseType === 'text') {
|
|
171
|
+
return typeof responseBody === 'string' ? responseBody : '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return responseBody;
|
|
175
|
+
}
|
|
176
|
+
|
|
9
177
|
export const api = {
|
|
10
178
|
device: JSON.parse(localStorage.getItem('device') || '{}'),
|
|
11
179
|
masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
|
|
@@ -17,19 +185,25 @@ export const api = {
|
|
|
17
185
|
const apiEntryPoint = resolveApiEntryPoint(APP_ENV.API_ENTRYPOINT);
|
|
18
186
|
const entryPoint = apiEntryPoint + (apiEntryPoint.endsWith("/") ? "" : "/");
|
|
19
187
|
const url = new URL(uri.startsWith('/') ? uri.substring(1) : uri, entryPoint).href;
|
|
188
|
+
const basicAuth = encodeBasicAuth(APP_ENV.HTACCESS_USER, APP_ENV.HTACCESS_PASSWORD);
|
|
189
|
+
const headers = {
|
|
190
|
+
'API-TOKEN': token,
|
|
191
|
+
'App-Domain': appDomain,
|
|
192
|
+
'Accept': '*/*',
|
|
193
|
+
// IMPORTANTE: Deixe vazio para o Axios/Browser definir o boundary do multipart
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (basicAuth !== '') {
|
|
197
|
+
headers.Authorization = `Basic ${basicAuth}`;
|
|
198
|
+
}
|
|
20
199
|
|
|
21
200
|
return axios.post(url, formData, {
|
|
22
|
-
headers
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
fetch: async function (uri, options = {}) {
|
|
31
|
-
if (typeof options.headers === 'undefined')
|
|
32
|
-
Object.assign(options, { headers: new Headers() });
|
|
201
|
+
headers,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
fetch: async function (uri, options = {}) {
|
|
205
|
+
if (typeof options.headers === 'undefined')
|
|
206
|
+
Object.assign(options, { headers: new Headers() });
|
|
33
207
|
|
|
34
208
|
// Always refresh device from localStorage in case it was set after module load
|
|
35
209
|
try {
|
|
@@ -45,14 +219,19 @@ export const api = {
|
|
|
45
219
|
} catch (e) {
|
|
46
220
|
this.masterDevice = {};
|
|
47
221
|
}
|
|
48
|
-
|
|
49
|
-
let token = await this.getToken();
|
|
50
|
-
if (token) options.headers.set('API-TOKEN', token);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (options.
|
|
54
|
-
|
|
55
|
-
|
|
222
|
+
|
|
223
|
+
let token = await this.getToken();
|
|
224
|
+
if (token) options.headers.set('API-TOKEN', token);
|
|
225
|
+
|
|
226
|
+
const basicAuth = encodeBasicAuth(APP_ENV.HTACCESS_USER, APP_ENV.HTACCESS_PASSWORD);
|
|
227
|
+
if (basicAuth !== '' && !options.headers.has('Authorization')) {
|
|
228
|
+
options.headers.set('Authorization', `Basic ${basicAuth}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let bodyDeviceId = null;
|
|
232
|
+
if (options.body) {
|
|
233
|
+
if (typeof options.body === 'object') {
|
|
234
|
+
bodyDeviceId = options.body.device || null;
|
|
56
235
|
} else if (typeof options.body === 'string') {
|
|
57
236
|
try {
|
|
58
237
|
const parsedBody = JSON.parse(options.body);
|
|
@@ -71,10 +250,10 @@ export const api = {
|
|
|
71
250
|
options.headers.set('Accept', MIME_TYPE);
|
|
72
251
|
}
|
|
73
252
|
options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
|
|
74
|
-
|
|
75
|
-
if (options.body && typeof options.body != 'string') {
|
|
76
|
-
options.body = JSON.stringify(options.body);
|
|
77
|
-
}
|
|
253
|
+
|
|
254
|
+
if (options.body && typeof options.body != 'string') {
|
|
255
|
+
options.body = JSON.stringify(options.body);
|
|
256
|
+
}
|
|
78
257
|
if (options.params) {
|
|
79
258
|
uri = this.buildQueryString(uri, options);
|
|
80
259
|
}
|
|
@@ -142,14 +321,35 @@ export const api = {
|
|
|
142
321
|
}
|
|
143
322
|
return uri;
|
|
144
323
|
},
|
|
145
|
-
post: async function (uri, body = {}) {
|
|
146
|
-
const options = {
|
|
147
|
-
method: 'POST',
|
|
148
|
-
body: body,
|
|
149
|
-
};
|
|
150
|
-
return await this.fetch(uri, options);
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
return
|
|
154
|
-
},
|
|
155
|
-
}
|
|
324
|
+
post: async function (uri, body = {}) {
|
|
325
|
+
const options = {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
body: body,
|
|
328
|
+
};
|
|
329
|
+
return await this.fetch(uri, options);
|
|
330
|
+
},
|
|
331
|
+
loadSmokeIndex: async function (config = {}) {
|
|
332
|
+
return await requestSmoke('/tests', {}, config);
|
|
333
|
+
},
|
|
334
|
+
loadArtifactBlob: async function (config = {}, artifact) {
|
|
335
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
336
|
+
throw new Error('Artefato inválido.');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!artifact.url) {
|
|
340
|
+
throw new Error('Artefato inválido.');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return await requestSmoke(artifact.url, {
|
|
344
|
+
responseType: 'blob',
|
|
345
|
+
}, config);
|
|
346
|
+
},
|
|
347
|
+
triggerSmokeRun: async function (config = {}) {
|
|
348
|
+
return await requestSmoke('/tests/run', {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
}, config);
|
|
351
|
+
},
|
|
352
|
+
execute: function (params) {
|
|
353
|
+
return axios(params);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
12
12
|
import * as DocumentPicker from 'expo-document-picker';
|
|
13
13
|
import { useStore } from '@store';
|
|
14
|
-
import AnimatedModal from '
|
|
14
|
+
import AnimatedModal from './AnimatedModal';
|
|
15
15
|
import { api } from '@controleonline/ui-common/src/api';
|
|
16
16
|
import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
|
|
17
17
|
import styles from './AddImportModal.styles';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Dimensions,
|
|
5
|
+
Keyboard,
|
|
6
|
+
KeyboardAvoidingView,
|
|
7
|
+
Modal,
|
|
8
|
+
Platform,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Text,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import {GestureHandlerRootView} from 'react-native-gesture-handler';
|
|
13
|
+
import {toast, Toasts} from '@backpackapp-io/react-native-toast';
|
|
14
|
+
import styles from './AnimatedModal.styles';
|
|
15
|
+
import {
|
|
16
|
+
TOAST_EXTRA_INSETS,
|
|
17
|
+
TOAST_MODAL_COUNT_KEY,
|
|
18
|
+
TOAST_PROVIDER_KEYS,
|
|
19
|
+
} from './toastConfig';
|
|
20
|
+
|
|
21
|
+
const {height: SCREEN_HEIGHT} = Dimensions.get('window');
|
|
22
|
+
|
|
23
|
+
const incrementModalToastDepth = () => {
|
|
24
|
+
const currentDepth = Number(global?.[TOAST_MODAL_COUNT_KEY] || 0);
|
|
25
|
+
global[TOAST_MODAL_COUNT_KEY] = currentDepth + 1;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const decrementModalToastDepth = () => {
|
|
29
|
+
const currentDepth = Number(global?.[TOAST_MODAL_COUNT_KEY] || 0);
|
|
30
|
+
global[TOAST_MODAL_COUNT_KEY] = Math.max(0, currentDepth - 1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Modal com overlay aparecendo na hora e apenas o conteúdo subindo de baixo.
|
|
35
|
+
*/
|
|
36
|
+
export default function AnimatedModal({
|
|
37
|
+
visible,
|
|
38
|
+
onRequestClose,
|
|
39
|
+
children,
|
|
40
|
+
style,
|
|
41
|
+
}) {
|
|
42
|
+
const [showing, setShowing] = useState(visible);
|
|
43
|
+
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
44
|
+
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
|
45
|
+
const slideY = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (visible) {
|
|
49
|
+
setShowing(true);
|
|
50
|
+
slideY.setValue(SCREEN_HEIGHT);
|
|
51
|
+
overlayOpacity.setValue(0);
|
|
52
|
+
|
|
53
|
+
Animated.parallel([
|
|
54
|
+
Animated.spring(slideY, {
|
|
55
|
+
toValue: 0,
|
|
56
|
+
damping: 24,
|
|
57
|
+
stiffness: 300,
|
|
58
|
+
useNativeDriver: true,
|
|
59
|
+
}),
|
|
60
|
+
Animated.timing(overlayOpacity, {
|
|
61
|
+
toValue: 1,
|
|
62
|
+
duration: 300,
|
|
63
|
+
useNativeDriver: true,
|
|
64
|
+
}),
|
|
65
|
+
]).start();
|
|
66
|
+
}
|
|
67
|
+
}, [visible, overlayOpacity, slideY]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!visible && showing) {
|
|
71
|
+
Animated.parallel([
|
|
72
|
+
Animated.timing(overlayOpacity, {
|
|
73
|
+
toValue: 0,
|
|
74
|
+
duration: 150,
|
|
75
|
+
useNativeDriver: true,
|
|
76
|
+
}),
|
|
77
|
+
Animated.timing(slideY, {
|
|
78
|
+
toValue: SCREEN_HEIGHT,
|
|
79
|
+
duration: 200,
|
|
80
|
+
useNativeDriver: true,
|
|
81
|
+
}),
|
|
82
|
+
]).start(() => {
|
|
83
|
+
setShowing(false);
|
|
84
|
+
setKeyboardHeight(0);
|
|
85
|
+
onRequestClose?.();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}, [visible, showing, overlayOpacity, slideY, onRequestClose]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (Platform.OS !== 'android') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const showSub = Keyboard.addListener('keyboardDidShow', event => {
|
|
96
|
+
setKeyboardHeight(event?.endCoordinates?.height || 0);
|
|
97
|
+
});
|
|
98
|
+
const hideSub = Keyboard.addListener('keyboardDidHide', () => {
|
|
99
|
+
setKeyboardHeight(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
showSub.remove();
|
|
104
|
+
hideSub.remove();
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!visible) {
|
|
110
|
+
setKeyboardHeight(0);
|
|
111
|
+
}
|
|
112
|
+
}, [visible]);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!showing) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
incrementModalToastDepth();
|
|
120
|
+
return () => {
|
|
121
|
+
decrementModalToastDepth();
|
|
122
|
+
};
|
|
123
|
+
}, [showing]);
|
|
124
|
+
|
|
125
|
+
const overlayStyle = [
|
|
126
|
+
styles.overlay,
|
|
127
|
+
style,
|
|
128
|
+
Platform.OS === 'android' ? {paddingBottom: keyboardHeight} : null,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const normalizedChildren = React.Children.toArray(children).map(child => {
|
|
132
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
133
|
+
const text = String(child).trim();
|
|
134
|
+
if (!text || text === '.') return null;
|
|
135
|
+
return <Text>{text}</Text>;
|
|
136
|
+
}
|
|
137
|
+
return child;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Modal
|
|
142
|
+
visible={showing}
|
|
143
|
+
animationType="none"
|
|
144
|
+
transparent
|
|
145
|
+
statusBarTranslucent
|
|
146
|
+
onRequestClose={onRequestClose}>
|
|
147
|
+
<GestureHandlerRootView style={styles.gestureRoot}>
|
|
148
|
+
<KeyboardAvoidingView
|
|
149
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
150
|
+
style={overlayStyle}>
|
|
151
|
+
<Animated.View
|
|
152
|
+
style={[
|
|
153
|
+
StyleSheet.absoluteFillObject,
|
|
154
|
+
styles.overlayBg,
|
|
155
|
+
{opacity: overlayOpacity},
|
|
156
|
+
]}
|
|
157
|
+
/>
|
|
158
|
+
<Animated.View
|
|
159
|
+
style={[styles.content, {transform: [{translateY: slideY}]}]}>
|
|
160
|
+
{normalizedChildren}
|
|
161
|
+
</Animated.View>
|
|
162
|
+
<Toasts
|
|
163
|
+
providerKey={TOAST_PROVIDER_KEYS.MODAL}
|
|
164
|
+
extraInsets={TOAST_EXTRA_INSETS}
|
|
165
|
+
onToastPress={currentToast => toast.dismiss(currentToast.id)}
|
|
166
|
+
/>
|
|
167
|
+
</KeyboardAvoidingView>
|
|
168
|
+
</GestureHandlerRootView>
|
|
169
|
+
</Modal>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {StyleSheet} from 'react-native';
|
|
2
|
+
|
|
3
|
+
const styles = StyleSheet.create({
|
|
4
|
+
gestureRoot: {
|
|
5
|
+
flex: 1,
|
|
6
|
+
},
|
|
7
|
+
overlay: {
|
|
8
|
+
flex: 1,
|
|
9
|
+
justifyContent: 'flex-end',
|
|
10
|
+
},
|
|
11
|
+
overlayBg: {
|
|
12
|
+
backgroundColor: 'rgba(15, 23, 42, 0.4)',
|
|
13
|
+
},
|
|
14
|
+
content: {
|
|
15
|
+
width: '100%',
|
|
16
|
+
flex: 1,
|
|
17
|
+
justifyContent: 'flex-end',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export default styles;
|