@controleonline/ui-common 1.2.101 → 1.2.102
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/.release/rc-manifest.json +7 -7
- package/docs/homepage-1056.md +1 -5
- package/package.json +1 -1
- package/src/api/index.js +40 -54
- package/src/react/components/AnimatedModal.js +6 -1
- package/src/react/components/BottomNavigationBar.js +6 -5
- package/src/react/components/BottomNavigationBar.soft.styles.js +5 -4
- package/src/react/components/ProductCatalogCacheService.js +53 -45
- package/src/react/components/RuntimeBottomNavigationBar.js +11 -4
- package/src/react/components/UserAvatar.js +7 -16
- package/src/react/hooks/useBottomNavigationPreference.js +49 -0
- package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +6 -4
- package/src/react/pages/Devices/deviceTypes/shared.js +9 -4
- package/src/react/pages/Devices.js +184 -183
- package/src/react/pages/IntegrationConfigPage.js +59 -13
- package/src/react/pages/IntegrationConfigPage.utils.js +17 -0
- package/src/react/pages/integrationsCatalog.js +71 -0
- package/src/react/utils/fileUrl.js +7 -1
- package/src/react/utils/productCatalogCacheRevision.js +13 -0
- package/src/tests/react/components/UserAvatar.test.js +30 -0
- package/src/tests/react/utils/fileUrl.test.js +23 -6
- package/src/tests/react/utils/runtimeFooter.test.js +189 -189
- package/src/tests/react/utils/storedSessionToken.test.js +22 -0
- package/src/utils/integrationConfigs.js +23 -0
- package/src/utils/storedSessionToken.js +18 -0
- package/src/utils/trustedApiUrl.js +19 -0
|
@@ -12,6 +12,8 @@ jest.mock('react-native', () => ({
|
|
|
12
12
|
View: props => React.createElement('View', props, props.children),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
|
+
jest.mock('@env', () => ({env: {API_ENTRYPOINT: 'https://api.controleonline.com', DOMAIN: 'manager.controleonline.com'}}));
|
|
16
|
+
|
|
15
17
|
const UserAvatar = require('../../../react/components/UserAvatar').default;
|
|
16
18
|
|
|
17
19
|
const {describe, expect, it} = global;
|
|
@@ -54,3 +56,31 @@ describe('UserAvatar', () => {
|
|
|
54
56
|
expect(image.props.source.uri).toContain('d=404');
|
|
55
57
|
});
|
|
56
58
|
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
describe('avatar credential boundary', () => {
|
|
62
|
+
it('does not authenticate an external download-shaped image on web', async () => {
|
|
63
|
+
const {Platform} = require('react-native');
|
|
64
|
+
const previousStorage = global.localStorage;
|
|
65
|
+
const previousFetch = global.fetch;
|
|
66
|
+
Platform.OS = 'web';
|
|
67
|
+
global.localStorage = {getItem: () => JSON.stringify({token: 'session-secret'})};
|
|
68
|
+
global.fetch = jest.fn();
|
|
69
|
+
let tree;
|
|
70
|
+
try {
|
|
71
|
+
await renderer.act(async () => { tree = renderer.create(React.createElement(UserAvatar, {imageUrl: 'https://images.example.test/files/1/download', useGravatar: false})); });
|
|
72
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
73
|
+
expect(tree.root.findByType('Image').props.source.uri).toBe('https://images.example.test/files/1/download');
|
|
74
|
+
} finally {
|
|
75
|
+
if (tree) await renderer.act(async () => tree.unmount());
|
|
76
|
+
Platform.OS = 'ios'; global.localStorage = previousStorage; global.fetch = previousFetch;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it('removes session headers from external native avatars', async () => {
|
|
80
|
+
let tree;
|
|
81
|
+
try {
|
|
82
|
+
await renderer.act(async () => { tree = renderer.create(React.createElement(UserAvatar, {imageUrl: 'https://images.example.test/avatar.png', imageHeaders: {'API-TOKEN': 'session-secret', Accept: 'image/*'}, useGravatar: false})); });
|
|
83
|
+
expect(tree.root.findByType('Image').props.source.headers).toEqual({Accept: 'image/*'});
|
|
84
|
+
} finally { if (tree) await renderer.act(async () => tree.unmount()); }
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -60,7 +60,7 @@ describe('fileUrl helpers', () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it('
|
|
63
|
+
it('keeps the explicit environment domain on the registered download route', () => {
|
|
64
64
|
const source = resolveDefaultFileSource(
|
|
65
65
|
{
|
|
66
66
|
id: 8444,
|
|
@@ -77,24 +77,41 @@ describe('fileUrl helpers', () => {
|
|
|
77
77
|
);
|
|
78
78
|
|
|
79
79
|
expect(source).toEqual({
|
|
80
|
-
uri: 'https://api.controleonline.com/files/8444/download',
|
|
80
|
+
uri: 'https://api.controleonline.com/staging.controleonline.com/files/8444/download',
|
|
81
81
|
headers: {
|
|
82
82
|
Authorization: 'Bearer token',
|
|
83
83
|
'app-domain': 'staging.controleonline.com',
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
|
-
expect(source.uri).
|
|
86
|
+
expect(source.uri).toContain('staging.controleonline.com/files');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('
|
|
89
|
+
it('uses the configured platform domain on the registered download route', () => {
|
|
90
90
|
// DOMAIN mock is manager.controleonline.com; no company → environment-like
|
|
91
91
|
const source = resolveDefaultFileSource(
|
|
92
92
|
{id: 9, url: '/files/9/download'},
|
|
93
93
|
{headers: {Authorization: 'Bearer t'}},
|
|
94
94
|
);
|
|
95
95
|
|
|
96
|
-
expect(source.uri).toBe('https://api.controleonline.com/files/9/download');
|
|
96
|
+
expect(source.uri).toBe('https://api.controleonline.com/manager.controleonline.com/files/9/download');
|
|
97
97
|
expect(source.headers['app-domain']).toBe('manager.controleonline.com');
|
|
98
|
-
expect(source.uri).
|
|
98
|
+
expect(source.uri).toMatch(/manager\.controleonline\.com\/files/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
describe('file authentication origin boundary', () => {
|
|
104
|
+
it('removes ERP credentials from external images and download-shaped URLs', () => {
|
|
105
|
+
for (const uri of ['https://images.example.test/image.png', 'https://images.example.test/files/1/download', '//images.example.test/image.png']) {
|
|
106
|
+
const source = resolveDefaultFileSource(uri, {headers: {'API-TOKEN': 'session-secret', Authorization: 'Bearer session-secret', 'App-Domain': 'tenant.test', Accept: 'image/*'}});
|
|
107
|
+
expect(source.headers).not.toHaveProperty('API-TOKEN');
|
|
108
|
+
expect(source.headers).not.toHaveProperty('Authorization');
|
|
109
|
+
expect(source.headers).not.toHaveProperty('App-Domain');
|
|
110
|
+
expect(source.headers.Accept).toBe('image/*');
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it('preserves authentication on the configured API download', () => {
|
|
114
|
+
const source = resolveDefaultFileSource(1, {headers: {'API-TOKEN': 'session-secret'}});
|
|
115
|
+
expect(source.headers['API-TOKEN']).toBe('session-secret');
|
|
99
116
|
});
|
|
100
117
|
});
|
|
@@ -30,17 +30,17 @@ const setLocation = value => {
|
|
|
30
30
|
Object.defineProperty(globalThis, 'location', {
|
|
31
31
|
configurable: true,
|
|
32
32
|
enumerable: true,
|
|
33
|
-
writable: true,
|
|
34
|
-
value,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const restoreLocation = () => {
|
|
39
|
-
if (originalLocationDescriptor) {
|
|
40
|
-
Object.defineProperty(globalThis, 'location', originalLocationDescriptor)
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
33
|
+
writable: true,
|
|
34
|
+
value,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const restoreLocation = () => {
|
|
39
|
+
if (originalLocationDescriptor) {
|
|
40
|
+
Object.defineProperty(globalThis, 'location', originalLocationDescriptor)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
44
|
delete globalThis.location
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -59,101 +59,101 @@ after(() => {
|
|
|
59
59
|
|
|
60
60
|
test('prefers the persisted public ip for web runtime devices', () => {
|
|
61
61
|
setLocation({hostname: '10.0.0.15'})
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
assert.equal(
|
|
65
|
-
getRuntimeFooterPrimaryText({
|
|
66
|
-
device: {
|
|
67
|
-
alias: 'app',
|
|
68
|
-
appVersion: '1.3.6',
|
|
69
|
-
deviceType: 'web',
|
|
70
|
-
id: 'web-12',
|
|
71
|
-
externalIp: '198.51.100.21',
|
|
72
|
-
metadata: {
|
|
73
|
-
network: {
|
|
74
|
-
publicIp: '198.51.100.21',
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
appVersion: '1.3.6',
|
|
79
|
-
deviceConfig: {
|
|
80
|
-
device: {
|
|
81
|
-
metadata: {
|
|
82
|
-
network: {
|
|
83
|
-
publicIp: '203.0.113.42',
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
configs: {
|
|
88
|
-
'config-version': '1.3.5',
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
'web (203.0.113.42) / v1.3.6',
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
const debugInfo = getRuntimeFooterDebugInfo({
|
|
96
|
-
device: {
|
|
97
|
-
alias: 'app',
|
|
98
|
-
appVersion: '1.3.6',
|
|
99
|
-
deviceType: 'web',
|
|
100
|
-
id: 'web-12',
|
|
101
|
-
externalIp: '198.51.100.21',
|
|
102
|
-
metadata: {
|
|
103
|
-
network: {
|
|
104
|
-
publicIp: '198.51.100.21',
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
appVersion: '1.3.6',
|
|
109
|
-
deviceConfig: {
|
|
110
|
-
device: {
|
|
111
|
-
metadata: {
|
|
112
|
-
network: {
|
|
113
|
-
publicIp: '203.0.113.42',
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
configs: {
|
|
118
|
-
'config-version': '1.3.5',
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
assert.equal(debugInfo.runtimeDetailSource, 'device_config.device.metadata.network.publicIp')
|
|
124
|
-
assert.equal(debugInfo.rawValues.storedDeviceMetadataPublicIp, '203.0.113.42')
|
|
125
|
-
assert.equal(debugInfo.rawValues.deviceMetadataPublicIp, '198.51.100.21')
|
|
126
|
-
} finally {
|
|
127
|
-
restoreLocation()
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('normalizes localhost to the loopback address on web', () => {
|
|
132
|
-
setLocation({hostname: 'localhost'})
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
assert.equal(getRuntimeFooterWebHost(), '127.0.0.1')
|
|
136
|
-
} finally {
|
|
137
|
-
restoreLocation()
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
assert.equal(
|
|
65
|
+
getRuntimeFooterPrimaryText({
|
|
66
|
+
device: {
|
|
67
|
+
alias: 'app',
|
|
68
|
+
appVersion: '1.3.6',
|
|
69
|
+
deviceType: 'web',
|
|
70
|
+
id: 'web-12',
|
|
71
|
+
externalIp: '198.51.100.21',
|
|
72
|
+
metadata: {
|
|
73
|
+
network: {
|
|
74
|
+
publicIp: '198.51.100.21',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
appVersion: '1.3.6',
|
|
79
|
+
deviceConfig: {
|
|
80
|
+
device: {
|
|
81
|
+
metadata: {
|
|
82
|
+
network: {
|
|
83
|
+
publicIp: '203.0.113.42',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
configs: {
|
|
88
|
+
'config-version': '1.3.5',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
'web (203.0.113.42) / v1.3.6',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const debugInfo = getRuntimeFooterDebugInfo({
|
|
96
|
+
device: {
|
|
97
|
+
alias: 'app',
|
|
98
|
+
appVersion: '1.3.6',
|
|
99
|
+
deviceType: 'web',
|
|
100
|
+
id: 'web-12',
|
|
101
|
+
externalIp: '198.51.100.21',
|
|
102
|
+
metadata: {
|
|
103
|
+
network: {
|
|
104
|
+
publicIp: '198.51.100.21',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
appVersion: '1.3.6',
|
|
109
|
+
deviceConfig: {
|
|
110
|
+
device: {
|
|
111
|
+
metadata: {
|
|
112
|
+
network: {
|
|
113
|
+
publicIp: '203.0.113.42',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
configs: {
|
|
118
|
+
'config-version': '1.3.5',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
assert.equal(debugInfo.runtimeDetailSource, 'device_config.device.metadata.network.publicIp')
|
|
124
|
+
assert.equal(debugInfo.rawValues.storedDeviceMetadataPublicIp, '203.0.113.42')
|
|
125
|
+
assert.equal(debugInfo.rawValues.deviceMetadataPublicIp, '198.51.100.21')
|
|
126
|
+
} finally {
|
|
127
|
+
restoreLocation()
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('normalizes localhost to the loopback address on web', () => {
|
|
132
|
+
setLocation({hostname: 'localhost'})
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
assert.equal(getRuntimeFooterWebHost(), '127.0.0.1')
|
|
136
|
+
} finally {
|
|
137
|
+
restoreLocation()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
141
|
test('falls back to the browser hostname for web when no persisted public ip exists', () => {
|
|
142
142
|
setLocation({hostname: 'localhost'})
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const candidates = getRuntimeFooterWebIdentifierCandidates({
|
|
146
|
-
device: {
|
|
147
|
-
deviceType: 'web',
|
|
148
|
-
id: 'web-12',
|
|
149
|
-
},
|
|
150
|
-
deviceConfig: {},
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
assert.equal(candidates[0]?.value, '127.0.0.1')
|
|
154
|
-
assert.equal(candidates[0]?.source, 'location.hostname')
|
|
155
|
-
} finally {
|
|
156
|
-
restoreLocation()
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const candidates = getRuntimeFooterWebIdentifierCandidates({
|
|
146
|
+
device: {
|
|
147
|
+
deviceType: 'web',
|
|
148
|
+
id: 'web-12',
|
|
149
|
+
},
|
|
150
|
+
deviceConfig: {},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assert.equal(candidates[0]?.value, '127.0.0.1')
|
|
154
|
+
assert.equal(candidates[0]?.source, 'location.hostname')
|
|
155
|
+
} finally {
|
|
156
|
+
restoreLocation()
|
|
157
157
|
}
|
|
158
158
|
})
|
|
159
159
|
|
|
@@ -191,91 +191,91 @@ test('recognizes backend web devices by their persisted runtime metadata or devi
|
|
|
191
191
|
|
|
192
192
|
test('reads the persisted config-version for non-web runtimes', () => {
|
|
193
193
|
assert.equal(
|
|
194
|
-
getRuntimeFooterStoredVersion({
|
|
195
|
-
configs: JSON.stringify({
|
|
196
|
-
'config-version': '1.3.5',
|
|
197
|
-
}),
|
|
198
|
-
}),
|
|
199
|
-
'v1.3.5',
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
assert.equal(
|
|
203
|
-
getRuntimeFooterPrimaryText({
|
|
204
|
-
device: {
|
|
205
|
-
alias: 'PDV',
|
|
206
|
-
appVersion: '1.3.7',
|
|
207
|
-
deviceType: 'android',
|
|
208
|
-
id: 'native-12',
|
|
209
|
-
},
|
|
210
|
-
appVersion: '1.3.6',
|
|
211
|
-
deviceConfig: {
|
|
212
|
-
device: {
|
|
213
|
-
device: '229252771069b294',
|
|
214
|
-
},
|
|
215
|
-
configs: {
|
|
216
|
-
'config-version': '1.3.5',
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
|
-
'PDV (229252771069b294) / v1.3.7',
|
|
221
|
-
)
|
|
222
|
-
})
|
|
223
|
-
|
|
194
|
+
getRuntimeFooterStoredVersion({
|
|
195
|
+
configs: JSON.stringify({
|
|
196
|
+
'config-version': '1.3.5',
|
|
197
|
+
}),
|
|
198
|
+
}),
|
|
199
|
+
'v1.3.5',
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert.equal(
|
|
203
|
+
getRuntimeFooterPrimaryText({
|
|
204
|
+
device: {
|
|
205
|
+
alias: 'PDV',
|
|
206
|
+
appVersion: '1.3.7',
|
|
207
|
+
deviceType: 'android',
|
|
208
|
+
id: 'native-12',
|
|
209
|
+
},
|
|
210
|
+
appVersion: '1.3.6',
|
|
211
|
+
deviceConfig: {
|
|
212
|
+
device: {
|
|
213
|
+
device: '229252771069b294',
|
|
214
|
+
},
|
|
215
|
+
configs: {
|
|
216
|
+
'config-version': '1.3.5',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
'PDV (229252771069b294) / v1.3.7',
|
|
221
|
+
)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
224
|
test('prefers the backend device identifier used by the devices page', () => {
|
|
225
225
|
assert.equal(
|
|
226
226
|
getRuntimeFooterPrimaryText({
|
|
227
227
|
device: {
|
|
228
228
|
alias: 'PDV',
|
|
229
|
-
appVersion: '1.3.7',
|
|
230
|
-
deviceType: 'android',
|
|
231
|
-
id: 'd41ac8afb9f178eb',
|
|
232
|
-
},
|
|
233
|
-
appVersion: '1.3.6',
|
|
234
|
-
deviceConfig: {
|
|
235
|
-
device: {
|
|
236
|
-
device: '229252771069b294',
|
|
237
|
-
metadata: {
|
|
238
|
-
app: {
|
|
239
|
-
version: '1.3.7',
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
configs: {
|
|
244
|
-
'config-version': '1.3.7',
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
}),
|
|
248
|
-
'PDV (229252771069b294) / v1.3.7',
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
const debugInfo = getRuntimeFooterDebugInfo({
|
|
252
|
-
device: {
|
|
253
|
-
alias: 'PDV',
|
|
254
|
-
appVersion: '1.3.7',
|
|
255
|
-
deviceType: 'android',
|
|
256
|
-
id: 'd41ac8afb9f178eb',
|
|
257
|
-
},
|
|
258
|
-
appVersion: '1.3.6',
|
|
259
|
-
deviceConfig: {
|
|
260
|
-
device: {
|
|
261
|
-
device: '229252771069b294',
|
|
262
|
-
metadata: {
|
|
263
|
-
app: {
|
|
264
|
-
version: '1.3.7',
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
configs: {
|
|
269
|
-
'config-version': '1.3.7',
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
assert.equal(debugInfo.versionLabel, 'v1.3.7')
|
|
275
|
-
assert.equal(debugInfo.displayName, 'PDV (229252771069b294)')
|
|
276
|
-
assert.equal(debugInfo.runtimeDetail, '229252771069b294')
|
|
277
|
-
assert.equal(debugInfo.runtimeDetailSource, 'device_config.device.device')
|
|
278
|
-
assert.equal(debugInfo.versionCandidates[0]?.source, 'device_config.device.metadata.app.version')
|
|
229
|
+
appVersion: '1.3.7',
|
|
230
|
+
deviceType: 'android',
|
|
231
|
+
id: 'd41ac8afb9f178eb',
|
|
232
|
+
},
|
|
233
|
+
appVersion: '1.3.6',
|
|
234
|
+
deviceConfig: {
|
|
235
|
+
device: {
|
|
236
|
+
device: '229252771069b294',
|
|
237
|
+
metadata: {
|
|
238
|
+
app: {
|
|
239
|
+
version: '1.3.7',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
configs: {
|
|
244
|
+
'config-version': '1.3.7',
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
'PDV (229252771069b294) / v1.3.7',
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const debugInfo = getRuntimeFooterDebugInfo({
|
|
252
|
+
device: {
|
|
253
|
+
alias: 'PDV',
|
|
254
|
+
appVersion: '1.3.7',
|
|
255
|
+
deviceType: 'android',
|
|
256
|
+
id: 'd41ac8afb9f178eb',
|
|
257
|
+
},
|
|
258
|
+
appVersion: '1.3.6',
|
|
259
|
+
deviceConfig: {
|
|
260
|
+
device: {
|
|
261
|
+
device: '229252771069b294',
|
|
262
|
+
metadata: {
|
|
263
|
+
app: {
|
|
264
|
+
version: '1.3.7',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
configs: {
|
|
269
|
+
'config-version': '1.3.7',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
assert.equal(debugInfo.versionLabel, 'v1.3.7')
|
|
275
|
+
assert.equal(debugInfo.displayName, 'PDV (229252771069b294)')
|
|
276
|
+
assert.equal(debugInfo.runtimeDetail, '229252771069b294')
|
|
277
|
+
assert.equal(debugInfo.runtimeDetailSource, 'device_config.device.device')
|
|
278
|
+
assert.equal(debugInfo.versionCandidates[0]?.source, 'device_config.device.metadata.app.version')
|
|
279
279
|
assert.equal(debugInfo.rawValues.deviceId, 'd41ac8afb9f178eb')
|
|
280
280
|
assert.equal(debugInfo.rawValues.storedDeviceIdentifier, '229252771069b294')
|
|
281
281
|
})
|
|
@@ -417,11 +417,11 @@ test('falls back to the local device id when the backend identifier is unavailab
|
|
|
417
417
|
const candidates = getRuntimeFooterNativeIdentifierCandidates({
|
|
418
418
|
device: {
|
|
419
419
|
id: 'local-device-99',
|
|
420
|
-
},
|
|
421
|
-
deviceConfig: {
|
|
422
|
-
device: {},
|
|
423
|
-
},
|
|
424
|
-
})
|
|
420
|
+
},
|
|
421
|
+
deviceConfig: {
|
|
422
|
+
device: {},
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
425
|
|
|
426
426
|
assert.equal(candidates[0]?.value, 'local-device-99')
|
|
427
427
|
assert.equal(candidates[0]?.source, 'device.id')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const {test} = require('node:test');
|
|
3
|
+
const {readStoredSessionToken} = require('../../../utils/storedSessionToken');
|
|
4
|
+
|
|
5
|
+
for (const prefix of ['', '__q_objt|']) {
|
|
6
|
+
for (const field of ['token', 'api_key']) {
|
|
7
|
+
test(`reads ${field} from ${prefix ? 'wrapped' : 'plain'} session`, () => {
|
|
8
|
+
const storage = {getItem: key => key === 'session' ? prefix + JSON.stringify({[field]: 'synthetic-token'}) : null};
|
|
9
|
+
assert.equal(readStoredSessionToken(storage), 'synthetic-token');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
test('prefers the canonical token field when both are present', () => {
|
|
14
|
+
assert.equal(readStoredSessionToken({getItem: () => JSON.stringify({token: 'current', api_key: 'old'})}), 'current');
|
|
15
|
+
});
|
|
16
|
+
test('missing, malformed and inaccessible sessions have no token', () => {
|
|
17
|
+
for (const raw of [null, '', 'broken', '__q_objt|broken', '{}', 'null']) {
|
|
18
|
+
assert.equal(readStoredSessionToken({getItem: () => raw}), '');
|
|
19
|
+
}
|
|
20
|
+
assert.equal(readStoredSessionToken({getItem: () => {throw new Error('unavailable');}}), '');
|
|
21
|
+
assert.equal(readStoredSessionToken(null), '');
|
|
22
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const CIELO_CONFIG_KEY = 'CIELO';
|
|
2
2
|
export const NEW_RELIC_CONFIG_KEY = 'NEW_RELIC';
|
|
3
3
|
export const SPOTIFY_CONFIG_KEY = 'SPOTIFY';
|
|
4
|
+
export const SEM_PARAR_CONFIG_KEY = 'SEM_PARAR';
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_CIELO_CONFIG = {
|
|
6
7
|
ACCESS_TOKEN: '',
|
|
@@ -23,6 +24,21 @@ export const DEFAULT_SPOTIFY_CONFIG = {
|
|
|
23
24
|
CLIENT_SECRET: '',
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Company-scoped Sem Parar credentials for vale-pedágio emission.
|
|
29
|
+
* ENABLED: "1" | "0"
|
|
30
|
+
* ENVIRONMENT: "sandbox" | "production"
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_SEM_PARAR_CONFIG = {
|
|
33
|
+
ENABLED: '0',
|
|
34
|
+
CLIENT_ID: '',
|
|
35
|
+
CLIENT_SECRET: '',
|
|
36
|
+
USERNAME: '',
|
|
37
|
+
PASSWORD: '',
|
|
38
|
+
ENVIRONMENT: 'sandbox',
|
|
39
|
+
CONVENIADO: '',
|
|
40
|
+
};
|
|
41
|
+
|
|
26
42
|
const isConfigMap = value =>
|
|
27
43
|
value && typeof value === 'object' && !Array.isArray(value);
|
|
28
44
|
|
|
@@ -78,3 +94,10 @@ export const resolveSpotifyConfig = configs =>
|
|
|
78
94
|
SPOTIFY_CONFIG_KEY,
|
|
79
95
|
DEFAULT_SPOTIFY_CONFIG,
|
|
80
96
|
);
|
|
97
|
+
|
|
98
|
+
export const resolveSemPararConfig = configs =>
|
|
99
|
+
resolveIntegrationConfig(
|
|
100
|
+
configs,
|
|
101
|
+
SEM_PARAR_CONFIG_KEY,
|
|
102
|
+
DEFAULT_SEM_PARAR_CONFIG,
|
|
103
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Quasar LocalStorage serializes objects with this marker; plain JSON is also used.
|
|
2
|
+
const OBJECT_PREFIX = '__q_objt|';
|
|
3
|
+
|
|
4
|
+
const readStoredSessionToken = (storage = typeof localStorage === 'undefined' ? null : localStorage) => {
|
|
5
|
+
try {
|
|
6
|
+
const raw = storage?.getItem?.('session');
|
|
7
|
+
if (!raw) return '';
|
|
8
|
+
const json = typeof raw === 'string' && raw.startsWith(OBJECT_PREFIX)
|
|
9
|
+
? raw.slice(OBJECT_PREFIX.length)
|
|
10
|
+
: raw;
|
|
11
|
+
const session = JSON.parse(json);
|
|
12
|
+
return session?.token || session?.api_key || '';
|
|
13
|
+
} catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {readStoredSessionToken};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const isTrustedApiUrl = (uri, apiEntryPoint) => {
|
|
2
|
+
try {
|
|
3
|
+
const base = new URL(String(apiEntryPoint || ''));
|
|
4
|
+
const target = new URL(String(uri || ''), `${base.href.replace(/\/$/, '')}/`);
|
|
5
|
+
const prefix = base.pathname.replace(/\/$/, '');
|
|
6
|
+
return ['http:', 'https:'].includes(base.protocol)
|
|
7
|
+
&& target.origin === base.origin && !target.username && !target.password
|
|
8
|
+
&& (!prefix || target.pathname === prefix || target.pathname.startsWith(`${prefix}/`));
|
|
9
|
+
} catch { return false; }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ERP credentials belong only to the configured API, never an image/CDN origin.
|
|
13
|
+
const filterApiHeaders = (uri, headers = {}, apiEntryPoint) => {
|
|
14
|
+
if (isTrustedApiUrl(uri, apiEntryPoint)) return {...headers};
|
|
15
|
+
return Object.fromEntries(Object.entries(headers).filter(([key]) =>
|
|
16
|
+
!/^(api[-_]?(token|key)|authorization|proxy-authorization|cookie|app-domain|device)$/i.test(key)));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = {isTrustedApiUrl, filterApiHeaders};
|