@applicaster/zapp-react-native-utils 16.0.0-alpha.9183418480 → 16.0.0-alpha.9534912292
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/actionsExecutor/ActionExecutorContext.tsx +27 -5
- package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
- package/analyticsUtils/PlayerAnalyticsManager.ts +11 -1
- package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
- package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
- package/analyticsUtils/__tests__/fixtures/index.js +20 -0
- package/analyticsUtils/analyticsMapper.ts +4 -1
- package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
- package/appUtils/HooksManager/index.ts +3 -8
- package/appUtils/contextKeysManager/utils/index.ts +38 -25
- package/cellUtils/index.ts +3 -5
- package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
- package/colorUtils/__tests__/isValidColor.test.ts +70 -0
- package/colorUtils/index.ts +50 -0
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
- package/manifestUtils/keys.js +1 -0
- package/manifestUtils/platformIsTV.js +1 -0
- package/package.json +2 -2
- package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
- package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
- package/pipesUtils/index.ts +1 -0
- package/pipesUtils/withPipesEndpoint.tsx +54 -0
- package/playerUtils/index.ts +4 -2
- package/reactHooks/cell-click/index.ts +15 -7
- package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
- package/reactHooks/feed/index.ts +5 -1
- package/reactHooks/feed/useBatchLoading.ts +9 -1
- package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
- package/reactHooks/utils/index.ts +3 -2
- package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
- package/riverComponetsMeasurementProvider/index.tsx +13 -13
- package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./ActionExecutor";
|
|
8
8
|
import {
|
|
9
9
|
batchRemoveAllFromNamespaceForStorage,
|
|
10
|
+
batchRemoveFromStorage,
|
|
10
11
|
batchSave,
|
|
11
12
|
} from "../zappFrameworkUtils/localStorageHelper";
|
|
12
13
|
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
|
|
@@ -38,13 +39,17 @@ import {
|
|
|
38
39
|
} from "./StorageActions";
|
|
39
40
|
|
|
40
41
|
import { screenSetVariable, screenToggleFlag } from "./ScreenActions";
|
|
42
|
+
import {
|
|
43
|
+
StorageValuesToAdd,
|
|
44
|
+
StorageValuesToRemove,
|
|
45
|
+
} from "../zappFrameworkUtils/types";
|
|
41
46
|
|
|
42
47
|
export const { log_error, log_info, log_debug } = createLogger({
|
|
43
48
|
subsystem: "ActionExecutorContext",
|
|
44
49
|
category: "General",
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
type ActionExecutorContextType = {
|
|
52
|
+
export type ActionExecutorContextType = {
|
|
48
53
|
registerAction: (
|
|
49
54
|
type: string,
|
|
50
55
|
handler: (
|
|
@@ -66,6 +71,7 @@ type ActionExecutorContextType = {
|
|
|
66
71
|
context?: Record<string, any>
|
|
67
72
|
) => Promise<ActionResult>;
|
|
68
73
|
};
|
|
74
|
+
|
|
69
75
|
type Props = {
|
|
70
76
|
children: React.ReactNode;
|
|
71
77
|
};
|
|
@@ -88,7 +94,7 @@ function findParentComponent(
|
|
|
88
94
|
|
|
89
95
|
const prepareDefaultActions = (actionExecutor) => {
|
|
90
96
|
actionExecutor.registerAction("localStorageSet", async (action) => {
|
|
91
|
-
const namespaces = action.options.content;
|
|
97
|
+
const namespaces: StorageValuesToAdd = action.options.content;
|
|
92
98
|
await batchSave(namespaces, localStorage);
|
|
93
99
|
// TODO: Add support for ownershipKey and ownershipNamespace
|
|
94
100
|
|
|
@@ -96,13 +102,29 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
96
102
|
});
|
|
97
103
|
|
|
98
104
|
actionExecutor.registerAction("sessionStorageSet", async (action) => {
|
|
99
|
-
const namespaces = action.options.content;
|
|
105
|
+
const namespaces: StorageValuesToAdd = action.options.content;
|
|
100
106
|
await batchSave(namespaces, sessionStorage);
|
|
101
107
|
// TODO: Add support for ownershipKey and ownershipNamespace
|
|
102
108
|
|
|
103
109
|
return ActionResult.Success;
|
|
104
110
|
});
|
|
105
111
|
|
|
112
|
+
actionExecutor.registerAction("localStorageRemove", async (action) => {
|
|
113
|
+
const namespaces: StorageValuesToRemove = action.options.content;
|
|
114
|
+
await batchRemoveFromStorage(namespaces, localStorage);
|
|
115
|
+
// TODO: Add support for ownershipKey and ownershipNamespace
|
|
116
|
+
|
|
117
|
+
return ActionResult.Success;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
actionExecutor.registerAction("sessionStorageRemove", async (action) => {
|
|
121
|
+
const namespaces: StorageValuesToRemove = action.options.content;
|
|
122
|
+
await batchRemoveFromStorage(namespaces, sessionStorage);
|
|
123
|
+
// TODO: Add support for ownershipKey and ownershipNamespace
|
|
124
|
+
|
|
125
|
+
return ActionResult.Success;
|
|
126
|
+
});
|
|
127
|
+
|
|
106
128
|
actionExecutor.registerAction(
|
|
107
129
|
"refreshComponent",
|
|
108
130
|
async (_action, context) => {
|
|
@@ -129,7 +151,7 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
129
151
|
getInflatedDataSourceUrl({
|
|
130
152
|
source,
|
|
131
153
|
contexts: {
|
|
132
|
-
entry: context?.
|
|
154
|
+
entry: context?.screenEntry,
|
|
133
155
|
screen: context?.screenData,
|
|
134
156
|
search: getSearchContext(null, mapping),
|
|
135
157
|
},
|
|
@@ -198,7 +220,7 @@ const prepareDefaultActions = (actionExecutor) => {
|
|
|
198
220
|
|
|
199
221
|
const entry = context?.entry || {};
|
|
200
222
|
const entryResolver = new EntryResolver(entry);
|
|
201
|
-
const screenData = context?.screenStateStore
|
|
223
|
+
const screenData = context?.screenStateStore?.getState().data || {};
|
|
202
224
|
const screenResolver = new EntryResolver(screenData || {});
|
|
203
225
|
|
|
204
226
|
const data =
|
|
@@ -6,9 +6,9 @@ import { postAnalyticEvent } from "../manager";
|
|
|
6
6
|
import { ANALYTICS_CORE_EVENTS } from "../events";
|
|
7
7
|
|
|
8
8
|
type AnalyticsDefaultHelperProperties = {
|
|
9
|
-
analyticsScreenData
|
|
9
|
+
analyticsScreenData?: AnalyticsScreenProperties;
|
|
10
10
|
extraProps: any;
|
|
11
|
-
props;
|
|
11
|
+
props?: any;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const sendMenuClickEvent = ({
|
|
@@ -23,6 +23,15 @@ class PlayerAnalyticsTrackerFactory {
|
|
|
23
23
|
return new PlayerAnalyticsTracker();
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
onPlayerRegistered(tracker: PlayerAnalyticsTrackerI, player: Player) {
|
|
28
|
+
tracker.setPlayer(player);
|
|
29
|
+
tracker.onPlayerPresented();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onPlayerUnRegistered(tracker?: PlayerAnalyticsTrackerI) {
|
|
33
|
+
tracker?.onPlayerClosed();
|
|
34
|
+
}
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export class PlayerAnalyticsManager implements PlayerLifecycleListener {
|
|
@@ -70,7 +79,7 @@ export class PlayerAnalyticsManager implements PlayerLifecycleListener {
|
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
const tracker = this.getTracker(player.playerId);
|
|
73
|
-
|
|
82
|
+
this.trackerFactory.onPlayerRegistered(tracker, player);
|
|
74
83
|
|
|
75
84
|
const analyticsListener = new AnalyticPlayerListener({
|
|
76
85
|
analyticsTracker: tracker,
|
|
@@ -84,6 +93,7 @@ export class PlayerAnalyticsManager implements PlayerLifecycleListener {
|
|
|
84
93
|
};
|
|
85
94
|
|
|
86
95
|
onUnRegistered = (player: Player) => {
|
|
96
|
+
this.trackerFactory.onPlayerUnRegistered(this.trackers[player.playerId]);
|
|
87
97
|
player.removeListener(AnalyticPlayerListener.analyticsListenerID);
|
|
88
98
|
delete this.trackers[player.playerId];
|
|
89
99
|
};
|
|
@@ -74,4 +74,39 @@ describe("Analytics Mapper", () => {
|
|
|
74
74
|
const rules = fixtures.ACPEventRules;
|
|
75
75
|
await setConfigRunMapTest(rules, fixtures.ACPEventEvents);
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
it("Does not let an ignore rule short-circuit later events", async () => {
|
|
79
|
+
await setConfigRunMapTest(
|
|
80
|
+
fixtures.ignoreOrderingRules,
|
|
81
|
+
fixtures.ignoreOrderingEvents
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("Keeps the original name when rename is empty", async () => {
|
|
86
|
+
await setConfigRunMapTest(
|
|
87
|
+
fixtures.emptyRenameRules,
|
|
88
|
+
fixtures.emptyRenameEvents
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("Renames via a regex with no named groups", async () => {
|
|
93
|
+
await setConfigRunMapTest(
|
|
94
|
+
fixtures.regexNonNamedFullMatchRules,
|
|
95
|
+
fixtures.regexNonNamedFullMatchEvents
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("Substitutes every named group in a regex rename", async () => {
|
|
100
|
+
await setConfigRunMapTest(
|
|
101
|
+
fixtures.regexMultiGroupRenameRules,
|
|
102
|
+
fixtures.regexMultiGroupRenameEvents
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("Does not fall back to event match when a regex rule does not match", async () => {
|
|
107
|
+
await setConfigRunMapTest(
|
|
108
|
+
fixtures.regexEventNoFallbackRules,
|
|
109
|
+
fixtures.regexEventNoFallbackEvents
|
|
110
|
+
);
|
|
111
|
+
});
|
|
77
112
|
});
|
|
@@ -2,17 +2,11 @@
|
|
|
2
2
|
{
|
|
3
3
|
"first": {
|
|
4
4
|
"name": "Screen viewed: Ads & redirects",
|
|
5
|
-
"params": {
|
|
6
|
-
"ignored_param": "removed_value",
|
|
7
|
-
"kept_param": "kept_value"
|
|
8
|
-
}
|
|
5
|
+
"params": {}
|
|
9
6
|
},
|
|
10
7
|
"second": {
|
|
11
8
|
"name": "Screen Ads & redirects",
|
|
12
|
-
"params": {
|
|
13
|
-
"ignored_param": "removed_value",
|
|
14
|
-
"kept_param": "kept_value"
|
|
15
|
-
}
|
|
9
|
+
"params": {}
|
|
16
10
|
}
|
|
17
11
|
}
|
|
18
|
-
]
|
|
12
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"event": "player_presented",
|
|
5
|
+
"ignore": true,
|
|
6
|
+
"strategy": "allowUnlisted"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"event": "screen_view",
|
|
10
|
+
"ignore": false,
|
|
11
|
+
"rename": "page_view",
|
|
12
|
+
"strategy": "allowUnlisted"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"strategy": "allowUnlisted"
|
|
16
|
+
}
|
|
@@ -14,6 +14,16 @@ import blockUnlistedEventEvents from "./analytics_mapper_testBlockUnlistedEvents
|
|
|
14
14
|
import blockUnlistedEventRules from "./analytics_mapper_testBlockUnlistedEvents_rules.json";
|
|
15
15
|
import ACPEventEvents from "./analytics_mapper_testACP_events.json";
|
|
16
16
|
import ACPEventRules from "./analytics_mapper_testACP_rules.json";
|
|
17
|
+
import emptyRenameRules from "./analytics_mapper_testEmptyRenameKeepsName_rules.json";
|
|
18
|
+
import emptyRenameEvents from "./analytics_mapper_testEmptyRenameKeepsName_events.json";
|
|
19
|
+
import ignoreOrderingRules from "./analytics_mapper_testIgnoreOrdering_rules.json";
|
|
20
|
+
import ignoreOrderingEvents from "./analytics_mapper_testIgnoreOrdering_events.json";
|
|
21
|
+
import regexEventNoFallbackRules from "./analytics_mapper_testRegexEventNoFallback_rules.json";
|
|
22
|
+
import regexEventNoFallbackEvents from "./analytics_mapper_testRegexEventNoFallback_events.json";
|
|
23
|
+
import regexMultiGroupRenameRules from "./analytics_mapper_testRegexMultiGroupRename_rules.json";
|
|
24
|
+
import regexMultiGroupRenameEvents from "./analytics_mapper_testRegexMultiGroupRename_events.json";
|
|
25
|
+
import regexNonNamedFullMatchRules from "./analytics_mapper_testRegexNonNamedFullMatch_rules.json";
|
|
26
|
+
import regexNonNamedFullMatchEvents from "./analytics_mapper_testRegexNonNamedFullMatch_events.json";
|
|
17
27
|
|
|
18
28
|
export const fixtures = {
|
|
19
29
|
renameRules,
|
|
@@ -32,4 +42,14 @@ export const fixtures = {
|
|
|
32
42
|
blockUnlistedEventRules,
|
|
33
43
|
ACPEventEvents,
|
|
34
44
|
ACPEventRules,
|
|
45
|
+
emptyRenameRules,
|
|
46
|
+
emptyRenameEvents,
|
|
47
|
+
ignoreOrderingRules,
|
|
48
|
+
ignoreOrderingEvents,
|
|
49
|
+
regexEventNoFallbackRules,
|
|
50
|
+
regexEventNoFallbackEvents,
|
|
51
|
+
regexMultiGroupRenameRules,
|
|
52
|
+
regexMultiGroupRenameEvents,
|
|
53
|
+
regexNonNamedFullMatchRules,
|
|
54
|
+
regexNonNamedFullMatchEvents,
|
|
35
55
|
};
|
|
@@ -76,7 +76,10 @@ class StateHolder {
|
|
|
76
76
|
delete this.pluckedParams[prefix];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// Stringify so values match the native iOS/Android mappers (which coerce
|
|
80
|
+
// analyticsCustomProperties values to strings) — keeps GA4 data consistent
|
|
81
|
+
// across web/LG/Samsung and native platforms.
|
|
82
|
+
return this._cap?.[suffix]?.toString() ?? null;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
private async getFromLocalAndSession(
|
|
@@ -4,8 +4,14 @@ import { isTrue } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
|
4
4
|
import { postAnalyticEvent } from "./manager";
|
|
5
5
|
import { isLive } from "../playerUtils";
|
|
6
6
|
import { extensionsEvents } from "./AnalyticsEvents/helper";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AD_EVENT,
|
|
9
|
+
EVENT_TYPES,
|
|
10
|
+
GENERAL_EVENT,
|
|
11
|
+
PLAYER_DISPLAY_STATES,
|
|
12
|
+
} from "./events";
|
|
8
13
|
import { Player } from "../appUtils/playerManager/player";
|
|
14
|
+
import { isNil } from "@applicaster/zapp-react-native-utils/utils";
|
|
9
15
|
|
|
10
16
|
export interface PlayerAnalyticsTrackerI {
|
|
11
17
|
handleAnalyticEvent(
|
|
@@ -13,6 +19,8 @@ export interface PlayerAnalyticsTrackerI {
|
|
|
13
19
|
eventData?: QuickBrickPlayer.EventData
|
|
14
20
|
): void;
|
|
15
21
|
setPlayer(player: Player): void;
|
|
22
|
+
onPlayerPresented(): void;
|
|
23
|
+
onPlayerClosed(): void;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
@@ -26,6 +34,15 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
|
26
34
|
this.player = player;
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
onPlayerPresented() {
|
|
38
|
+
this.handleAnalyticEvent(GENERAL_EVENT.player_presented);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onPlayerClosed() {
|
|
42
|
+
this.handleAnalyticEvent(GENERAL_EVENT.session_end);
|
|
43
|
+
this.handleAnalyticEvent(GENERAL_EVENT.player_closed);
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
isAdBreak(event: string): boolean {
|
|
30
47
|
const adBreakEvents = [
|
|
31
48
|
AD_EVENT.ad_break_start,
|
|
@@ -180,7 +197,8 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
|
180
197
|
this.entry?.extensions?.free || this.entry?.extensions?.isFree;
|
|
181
198
|
|
|
182
199
|
const title = this.entry?.title;
|
|
183
|
-
const
|
|
200
|
+
const isStreamLive = isLive(this.entry);
|
|
201
|
+
const streamType = isStreamLive ? "live" : "vod"; // Todo: determine other types, channel, podcast, aod
|
|
184
202
|
|
|
185
203
|
const currentPosition = this.getCurrentPosition(
|
|
186
204
|
event,
|
|
@@ -201,6 +219,7 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
|
201
219
|
);
|
|
202
220
|
|
|
203
221
|
const playerState = this.getPlayerState();
|
|
222
|
+
const playerType = this.player?.playerPluginId;
|
|
204
223
|
|
|
205
224
|
const analyticsCustomProperties = extensionsEvents(this.entry?.extensions);
|
|
206
225
|
|
|
@@ -210,7 +229,11 @@ export class PlayerAnalyticsTracker implements PlayerAnalyticsTrackerI {
|
|
|
210
229
|
name: title,
|
|
211
230
|
media_type: mediaType,
|
|
212
231
|
stream_type: streamType,
|
|
213
|
-
|
|
232
|
+
player_type: playerType,
|
|
233
|
+
// We do not pass duration for live streams anymore (it's now seekableDuration),
|
|
234
|
+
// but we can't mark it as optional in Analytics Mapper yet,
|
|
235
|
+
// so we set it to -1 for live streams
|
|
236
|
+
duration: isStreamLive && isNil(mediaDuration) ? -1 : mediaDuration,
|
|
214
237
|
current_position: currentPosition,
|
|
215
238
|
player_state: playerState,
|
|
216
239
|
stream_format: contentType,
|
|
@@ -87,7 +87,7 @@ export function HooksManager({
|
|
|
87
87
|
}: HookManagerArgs): HookManager {
|
|
88
88
|
hooksManagerLogger.addContext({ targetScreenId: targetScreen.id });
|
|
89
89
|
|
|
90
|
-
function logHookEvent(func, message, data) {
|
|
90
|
+
function logHookEvent(func, message, data = {}) {
|
|
91
91
|
func({
|
|
92
92
|
message,
|
|
93
93
|
data: __DEV__ ? data : null,
|
|
@@ -344,7 +344,6 @@ export function HooksManager({
|
|
|
344
344
|
`hookCallback: send app to background, cancelled flow blocker hook ${hookPlugin.identifier} on home screen`,
|
|
345
345
|
{
|
|
346
346
|
payload,
|
|
347
|
-
hook: hookPlugin,
|
|
348
347
|
}
|
|
349
348
|
);
|
|
350
349
|
|
|
@@ -356,17 +355,13 @@ export function HooksManager({
|
|
|
356
355
|
`hookCallback: hook successfully finished: ${hookPlugin.identifier}`,
|
|
357
356
|
{
|
|
358
357
|
payload,
|
|
359
|
-
hook: hookPlugin,
|
|
360
358
|
}
|
|
361
359
|
);
|
|
362
360
|
|
|
363
361
|
if (!callback) {
|
|
364
362
|
logHookEvent(
|
|
365
|
-
hooksManagerLogger.
|
|
366
|
-
`hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)
|
|
367
|
-
{
|
|
368
|
-
hookPlugin,
|
|
369
|
-
}
|
|
363
|
+
hooksManagerLogger.debug,
|
|
364
|
+
`hookCallback: ${hookPlugin.identifier} is missing \`callback\`, using hookCallback(default one)`
|
|
370
365
|
);
|
|
371
366
|
|
|
372
367
|
callback = hookCallback;
|
|
@@ -1,40 +1,53 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
1
|
import { DEFAULT_NAMESPACE } from "../consts";
|
|
3
2
|
import { KeyName, KeyNameObj } from "../index";
|
|
4
3
|
|
|
5
|
-
const lastDotRegex = /\.([^.]+)$/;
|
|
6
|
-
const splitByLastDot = R.compose(R.init, R.split(lastDotRegex));
|
|
7
|
-
|
|
8
4
|
export function getNamespaceAndKey(namespacedKey: KeyName): KeyNameObj {
|
|
9
5
|
if (typeof namespacedKey !== "string") return namespacedKey;
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
if (!namespacedKey.includes(".")) {
|
|
8
|
+
return { namespace: DEFAULT_NAMESPACE, key: namespacedKey };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// split on the last dot - the namespace itself may contain dots
|
|
12
|
+
const lastDotIndex = namespacedKey.lastIndexOf(".");
|
|
14
13
|
|
|
15
|
-
return
|
|
14
|
+
return {
|
|
15
|
+
namespace: namespacedKey.slice(0, lastDotIndex),
|
|
16
|
+
key: namespacedKey.slice(lastDotIndex + 1),
|
|
17
|
+
};
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
+
const hasOwn = (obj: object, prop: string): boolean =>
|
|
21
|
+
Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
|
|
23
|
+
const isEmpty = (value: unknown): boolean => {
|
|
24
|
+
if (typeof value === "string" || Array.isArray(value)) {
|
|
25
|
+
return value.length === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (value !== null && typeof value === "object") {
|
|
29
|
+
return Object.keys(value).length === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const keyIsValid = (parsedKey: unknown): boolean => {
|
|
36
|
+
if (parsedKey == null) return false;
|
|
37
|
+
if (typeof parsedKey === "string") return false;
|
|
38
|
+
if (Array.isArray(parsedKey)) return false;
|
|
20
39
|
|
|
21
|
-
const
|
|
40
|
+
const obj = parsedKey as Record<string, unknown>;
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
propIsNotNullOrUndefined("key"),
|
|
30
|
-
propIsNotNullOrUndefined("namespace"),
|
|
31
|
-
propIsNotEmpty("key"),
|
|
32
|
-
propIsNotEmpty("namespace"),
|
|
33
|
-
]);
|
|
42
|
+
if (!hasOwn(obj, "key") || !hasOwn(obj, "namespace")) return false;
|
|
43
|
+
if (obj.key == null || obj.namespace == null) return false;
|
|
44
|
+
if (isEmpty(obj.key) || isEmpty(obj.namespace)) return false;
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
34
48
|
|
|
35
49
|
export const buildNamespaceKey = (key: string, namespace: string) =>
|
|
36
50
|
`${namespace}.${key}`;
|
|
37
51
|
|
|
38
|
-
export const savingResultIsSuccess = (result: unknown): boolean =>
|
|
39
|
-
|
|
40
|
-
};
|
|
52
|
+
export const savingResultIsSuccess = (result: unknown): boolean =>
|
|
53
|
+
typeof result === "boolean" && result === true;
|
package/cellUtils/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
2
|
import { dayjs } from "../dateUtils";
|
|
3
|
-
import validateColor from "validate-color";
|
|
4
3
|
|
|
4
|
+
import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
|
|
5
5
|
import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
|
|
6
6
|
import {
|
|
7
7
|
capitalize,
|
|
@@ -468,15 +468,13 @@ export const getColorFromData = ({
|
|
|
468
468
|
data,
|
|
469
469
|
valueFromLayout,
|
|
470
470
|
}: GetColorFromData): string => {
|
|
471
|
-
|
|
472
|
-
// https://github.com/dreamyguy/validate-color/issues/44
|
|
473
|
-
if (validateColor(valueFromLayout.replace(".00", ""))) {
|
|
471
|
+
if (isValidColor(valueFromLayout)) {
|
|
474
472
|
return valueFromLayout;
|
|
475
473
|
}
|
|
476
474
|
|
|
477
475
|
const pathValue = R.path(valueFromLayout.split("."), data);
|
|
478
476
|
|
|
479
|
-
if (pathValue &&
|
|
477
|
+
if (pathValue && isValidColor(pathValue)) {
|
|
480
478
|
return pathValue;
|
|
481
479
|
}
|
|
482
480
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isTransparentColor } from "..";
|
|
2
|
+
|
|
3
|
+
describe("isTransparentColor", () => {
|
|
4
|
+
it("returns true for transparent keyword and rgba colors with zero alpha", () => {
|
|
5
|
+
const transparentColors = [
|
|
6
|
+
"transparent",
|
|
7
|
+
"rgba(0,0,0,0)",
|
|
8
|
+
"rgba(255, 255, 255, 0)",
|
|
9
|
+
"rgba(0,0,0,0.0)",
|
|
10
|
+
"rgba(0,0,0,0.00)",
|
|
11
|
+
"rgba(0,0,0, 0)",
|
|
12
|
+
"hsla(0,0%,0%,0)",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
expect.assertions(transparentColors.length);
|
|
16
|
+
|
|
17
|
+
transparentColors.forEach((color) => {
|
|
18
|
+
expect(isTransparentColor(color)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns false for valid colors that are not fully transparent", () => {
|
|
23
|
+
const nonTransparentColors = [
|
|
24
|
+
"red",
|
|
25
|
+
"#fff",
|
|
26
|
+
"#ffffff",
|
|
27
|
+
"rgb(0,0,0)",
|
|
28
|
+
"rgba(0,0,0)",
|
|
29
|
+
"rgba(0,0,0,0.5)",
|
|
30
|
+
"rgba(255, 255, 255, 0.3)",
|
|
31
|
+
"rgba(255, 255, 255, 1.0)",
|
|
32
|
+
"rgba(239,239,239,1.0)",
|
|
33
|
+
"currentColor",
|
|
34
|
+
"inherit",
|
|
35
|
+
"#00000000",
|
|
36
|
+
"#fff0",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
expect.assertions(nonTransparentColors.length);
|
|
40
|
+
|
|
41
|
+
nonTransparentColors.forEach((color) => {
|
|
42
|
+
expect(isTransparentColor(color)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns false for case variants of the transparent keyword", () => {
|
|
47
|
+
expect(isTransparentColor("Transparent")).toBe(false);
|
|
48
|
+
expect(isTransparentColor("TRANSPARENT")).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false for invalid color strings", () => {
|
|
52
|
+
const invalidColors = [
|
|
53
|
+
"invalid",
|
|
54
|
+
"",
|
|
55
|
+
"#gggggg",
|
|
56
|
+
"#fff.00",
|
|
57
|
+
"unset",
|
|
58
|
+
" rgba(0,0,0,0) ",
|
|
59
|
+
"transparent ",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
expect.assertions(invalidColors.length);
|
|
63
|
+
|
|
64
|
+
invalidColors.forEach((color) => {
|
|
65
|
+
expect(isTransparentColor(color)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns false for nullish and non-string values", () => {
|
|
70
|
+
expect(isTransparentColor(undefined)).toBe(false);
|
|
71
|
+
expect(isTransparentColor(null)).toBe(false);
|
|
72
|
+
expect(isTransparentColor(123)).toBe(false);
|
|
73
|
+
expect(isTransparentColor({})).toBe(false);
|
|
74
|
+
expect(isTransparentColor([])).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isValidColor } from "..";
|
|
2
|
+
|
|
3
|
+
describe("isValidColor", () => {
|
|
4
|
+
it("returns true for valid hex colors", () => {
|
|
5
|
+
const validHexColors = ["#fff", "#ffffff", "#FF0000", "#000", "#abc123"];
|
|
6
|
+
|
|
7
|
+
expect.assertions(validHexColors.length);
|
|
8
|
+
|
|
9
|
+
validHexColors.forEach((color) => {
|
|
10
|
+
expect(isValidColor(color)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns true for valid rgb and rgba colors", () => {
|
|
15
|
+
const validRgbColors = [
|
|
16
|
+
"rgb(255,0,0)",
|
|
17
|
+
"rgb(255, 0, 0)",
|
|
18
|
+
"rgba(255,0,0,0.5)",
|
|
19
|
+
"rgba(255, 0, 0, 0.5)",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
expect.assertions(validRgbColors.length);
|
|
23
|
+
|
|
24
|
+
validRgbColors.forEach((color) => {
|
|
25
|
+
expect(isValidColor(color)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns true for valid named and special CSS colors", () => {
|
|
30
|
+
const validNamedColors = ["red", "transparent", "currentColor", "inherit"];
|
|
31
|
+
|
|
32
|
+
expect.assertions(validNamedColors.length);
|
|
33
|
+
|
|
34
|
+
validNamedColors.forEach((color) => {
|
|
35
|
+
expect(isValidColor(color)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns true for valid hsl colors", () => {
|
|
40
|
+
expect(isValidColor("hsl(0, 100%, 50%)")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns false for invalid color strings", () => {
|
|
44
|
+
const invalidColors = [
|
|
45
|
+
"invalid",
|
|
46
|
+
"",
|
|
47
|
+
"#gggggg",
|
|
48
|
+
"#fff.00",
|
|
49
|
+
"unset",
|
|
50
|
+
undefined,
|
|
51
|
+
null,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
expect.assertions(invalidColors.length);
|
|
55
|
+
|
|
56
|
+
invalidColors.forEach((color) => {
|
|
57
|
+
expect(isValidColor(color)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns true for valid rgb and rgba colors", () => {
|
|
62
|
+
const validRgbColors = ["rgba(239,239,239,1.0)"];
|
|
63
|
+
|
|
64
|
+
expect.assertions(validRgbColors.length);
|
|
65
|
+
|
|
66
|
+
validRgbColors.forEach((color) => {
|
|
67
|
+
expect(isValidColor(color)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import validateColor from "validate-color";
|
|
2
|
+
import { isNil } from "@applicaster/zapp-react-native-utils/utils";
|
|
3
|
+
import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
4
|
+
|
|
5
|
+
const normalizeRgbaAlpha = (color: string): string => {
|
|
6
|
+
return color.replace(
|
|
7
|
+
/^rgba\(([^)]+),\s*(\d+(?:\.\d+)?)\s*\)$/i,
|
|
8
|
+
(_, rgb, alpha) => {
|
|
9
|
+
const alphaValue = parseFloat(alpha);
|
|
10
|
+
|
|
11
|
+
if (!Number.isFinite(alphaValue) || !Number.isInteger(alphaValue)) {
|
|
12
|
+
return color;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `rgba(${rgb},${alphaValue})`;
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const isValidColor = (color: string): boolean => {
|
|
21
|
+
if (isNil(color) || !isString(color)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (validateColor(color)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// validate-color rejects integer alpha values written as floats (e.g. 1.0)
|
|
30
|
+
// https://github.com/dreamyguy/validate-color/issues/44
|
|
31
|
+
const normalizedColor = normalizeRgbaAlpha(color);
|
|
32
|
+
|
|
33
|
+
return normalizedColor !== color && validateColor(normalizedColor);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function isRgbaAlphaZero(color: string): boolean {
|
|
37
|
+
const layers = color
|
|
38
|
+
.replace("rgba(", "")
|
|
39
|
+
.replace(")", "")
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((layer) => layer.trim());
|
|
42
|
+
|
|
43
|
+
return Number(layers[3]) === 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const isTransparentColor = (color: string): boolean => {
|
|
47
|
+
return (
|
|
48
|
+
isValidColor(color) && (color === "transparent" || isRgbaAlphaZero(color))
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -308,6 +308,41 @@ const generalContent = () => ({
|
|
|
308
308
|
key: "pull_to_refresh_enabled",
|
|
309
309
|
initial_value: false,
|
|
310
310
|
},
|
|
311
|
+
{
|
|
312
|
+
type: "switch",
|
|
313
|
+
label: "Allow using this screen as a hook",
|
|
314
|
+
label_tooltip:
|
|
315
|
+
"Make sure that screen uses 'finishHook' action or performs navigation action to exit the screen after performing the hook action, or user will be stuck on the screen", // eslint-disable-line max-len
|
|
316
|
+
key: "available_as_hook",
|
|
317
|
+
initial_value: false,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
type: "data_source_selector",
|
|
321
|
+
label: "Skip hook endpoint",
|
|
322
|
+
key: "skip_hook_endpoint",
|
|
323
|
+
label_tooltip:
|
|
324
|
+
"If set, this endpoint will check with the server whether the hook should be skipped",
|
|
325
|
+
conditional_fields: [
|
|
326
|
+
{
|
|
327
|
+
key: "rules/available_as_hook",
|
|
328
|
+
condition_value: true,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
key: "skip_hook_storage_key",
|
|
334
|
+
type: "text_input",
|
|
335
|
+
label: "Hook will be skipped if storage key is set",
|
|
336
|
+
initial_value: "",
|
|
337
|
+
label_tooltip:
|
|
338
|
+
"Comma-separated keys in namespace.key format (key without a dot uses the default namespace)", // eslint-disable-line max-len
|
|
339
|
+
conditional_fields: [
|
|
340
|
+
{
|
|
341
|
+
key: "rules/available_as_hook",
|
|
342
|
+
condition_value: true,
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
311
346
|
],
|
|
312
347
|
},
|
|
313
348
|
});
|
package/manifestUtils/keys.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.9534912292",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "16.0.0-alpha.
|
|
30
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.9534912292",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buildUrlWithQuery } from "../withPipesEndpoint";
|
|
2
|
+
|
|
3
|
+
describe("buildUrlWithQuery", () => {
|
|
4
|
+
it("returns the url unchanged when requestParams is null", () => {
|
|
5
|
+
expect(buildUrlWithQuery("https://foo.com/path", null)).toBe(
|
|
6
|
+
"https://foo.com/path"
|
|
7
|
+
);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns the url unchanged when requestParams has no params", () => {
|
|
11
|
+
expect(buildUrlWithQuery("https://foo.com/path", { headers: {} })).toBe(
|
|
12
|
+
"https://foo.com/path"
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns the url unchanged when url is empty", () => {
|
|
17
|
+
expect(buildUrlWithQuery("", { params: { a: "1" } })).toBe("");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("appends params to a url with no existing query", () => {
|
|
21
|
+
expect(
|
|
22
|
+
buildUrlWithQuery("https://foo.com/path", { params: { token: "abc" } })
|
|
23
|
+
).toBe("https://foo.com/path?token=abc");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("merges params with an existing query string", () => {
|
|
27
|
+
expect(
|
|
28
|
+
buildUrlWithQuery("https://foo.com/path?existing=1", {
|
|
29
|
+
params: { token: "abc" },
|
|
30
|
+
})
|
|
31
|
+
).toBe("https://foo.com/path?existing=1&token=abc");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { Text } from "react-native";
|
|
4
|
+
|
|
5
|
+
const mockedUseBuildPipesUrl = jest.fn();
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed", () => ({
|
|
8
|
+
useBuildPipesUrl: (args) => mockedUseBuildPipesUrl(args),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { withPipesEndpoint } = require("../withPipesEndpoint");
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line react/display-name
|
|
14
|
+
const Wrapped = React.forwardRef((props: any, _ref) => (
|
|
15
|
+
<Text testID="wrapped">{`${props.uri}|${JSON.stringify(props.headers)}`}</Text>
|
|
16
|
+
));
|
|
17
|
+
|
|
18
|
+
describe("withPipesEndpoint", () => {
|
|
19
|
+
afterEach(() => jest.clearAllMocks());
|
|
20
|
+
|
|
21
|
+
it("renders nothing while the endpoint context is resolving", () => {
|
|
22
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: null });
|
|
23
|
+
|
|
24
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
25
|
+
const { queryByTestId } = render(<Decorated uri="https://foo.com" />);
|
|
26
|
+
|
|
27
|
+
expect(queryByTestId("wrapped")).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("passes the uri through unchanged when there are no params", () => {
|
|
31
|
+
mockedUseBuildPipesUrl.mockReturnValue({ requestParams: {} });
|
|
32
|
+
|
|
33
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
34
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
35
|
+
|
|
36
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
37
|
+
"https://foo.com/path|{}"
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("merges resolved params into the uri and forwards headers", () => {
|
|
42
|
+
mockedUseBuildPipesUrl.mockReturnValue({
|
|
43
|
+
requestParams: {
|
|
44
|
+
params: { token: "abc" },
|
|
45
|
+
headers: { Authorization: "Bearer xyz" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const Decorated = withPipesEndpoint(Wrapped);
|
|
50
|
+
const { getByTestId } = render(<Decorated uri="https://foo.com/path" />);
|
|
51
|
+
|
|
52
|
+
expect(getByTestId("wrapped").props.children).toBe(
|
|
53
|
+
'https://foo.com/path?token=abc|{"Authorization":"Bearer xyz"}'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withPipesEndpoint, buildUrlWithQuery } from "./withPipesEndpoint";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { forwardRef, RefObject } from "react";
|
|
2
|
+
import URL from "url";
|
|
3
|
+
|
|
4
|
+
import { useBuildPipesUrl } from "@applicaster/zapp-react-native-utils/reactHooks/feed";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges the query params resolved from a pipes endpoint context into the url.
|
|
8
|
+
* Returns the url unchanged when there is nothing to merge.
|
|
9
|
+
*/
|
|
10
|
+
export function buildUrlWithQuery(url: string, requestParams): string {
|
|
11
|
+
if (!url || !requestParams?.params) {
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parsedURL = URL.parse(url, true);
|
|
16
|
+
|
|
17
|
+
parsedURL.query = { ...parsedURL.query, ...requestParams.params };
|
|
18
|
+
parsedURL.search = null;
|
|
19
|
+
|
|
20
|
+
return URL.format(parsedURL);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
uri: string;
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HOC that resolves the wrapped component's `uri` against its matching Zapp
|
|
29
|
+
* Pipes endpoint. Endpoint context keys are added as query params (merged into
|
|
30
|
+
* the uri) and as request `headers`. Rendering is deferred until the context
|
|
31
|
+
* has been resolved so the component never loads without its required headers.
|
|
32
|
+
*/
|
|
33
|
+
export function withPipesEndpoint(Component) {
|
|
34
|
+
function WithPipesEndpoint(props: Props, ref: RefObject<unknown>) {
|
|
35
|
+
const { requestParams } = useBuildPipesUrl({ url: props.uri });
|
|
36
|
+
|
|
37
|
+
if (requestParams !== null) {
|
|
38
|
+
const urlWithQuery = buildUrlWithQuery(props.uri, requestParams);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Component
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
uri={urlWithQuery}
|
|
45
|
+
headers={requestParams.headers || {}}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return forwardRef(WithPipesEndpoint);
|
|
54
|
+
}
|
package/playerUtils/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function isLiveLegacy(content) {
|
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
39
|
const durationFromExt = R.path(["extensions", "duration"], content);
|
|
40
|
-
const durationFromMgr = playerManager.getDuration();
|
|
40
|
+
const durationFromMgr = playerManager.getInstanceController()?.getDuration();
|
|
41
41
|
const duration = Math.floor(durationFromExt || durationFromMgr);
|
|
42
42
|
|
|
43
43
|
const isLive = R.anyPass([isNotaValidNumber, R.lte(R.__, 0)])(duration);
|
|
@@ -81,7 +81,9 @@ function isLiveByManager(): boolean {
|
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const durationFromPlayerManager = playerManager
|
|
84
|
+
const durationFromPlayerManager = playerManager
|
|
85
|
+
.getInstanceController()
|
|
86
|
+
?.getDuration();
|
|
85
87
|
|
|
86
88
|
return isLiveByDuration(durationFromPlayerManager);
|
|
87
89
|
}
|
|
@@ -49,12 +49,13 @@ export const useCellClick = ({
|
|
|
49
49
|
const actionExecutor = React.useContext(ActionExecutorContext);
|
|
50
50
|
const screenData = useCurrentScreenData();
|
|
51
51
|
const screenState = useScreenContext()?.options;
|
|
52
|
+
const entry = useScreenContext()?.entry;
|
|
52
53
|
|
|
53
54
|
const cellSelectable = toBooleanWithDefaultTrue(
|
|
54
55
|
component?.rules?.component_cells_selectable
|
|
55
56
|
);
|
|
56
57
|
|
|
57
|
-
const [
|
|
58
|
+
const [_entryContext, setEntryContext] =
|
|
58
59
|
ZappPipesEntryContext.useZappPipesContext(pathname);
|
|
59
60
|
|
|
60
61
|
const logTimestamp = useProfilerLogging();
|
|
@@ -89,7 +90,8 @@ export const useCellClick = ({
|
|
|
89
90
|
screenState,
|
|
90
91
|
screenRoute: pathname,
|
|
91
92
|
screenStateStore,
|
|
92
|
-
entryContext,
|
|
93
|
+
entryContext: selectedItem,
|
|
94
|
+
screenEntry: entry,
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -117,14 +119,20 @@ export const useCellClick = ({
|
|
|
117
119
|
}
|
|
118
120
|
},
|
|
119
121
|
[
|
|
120
|
-
|
|
121
|
-
currentRoute,
|
|
122
|
+
item,
|
|
122
123
|
setEntryContext,
|
|
123
|
-
pathname,
|
|
124
|
-
push,
|
|
125
124
|
sendAnalyticsOnPress,
|
|
125
|
+
logTimestamp,
|
|
126
|
+
pathname,
|
|
127
|
+
component,
|
|
128
|
+
onCellTap,
|
|
129
|
+
entry,
|
|
130
|
+
actionExecutor,
|
|
126
131
|
screenData,
|
|
127
132
|
screenState,
|
|
133
|
+
screenStateStore,
|
|
134
|
+
currentRoute,
|
|
135
|
+
push,
|
|
128
136
|
]
|
|
129
137
|
);
|
|
130
138
|
|
|
@@ -138,5 +146,5 @@ export const useCellClick = ({
|
|
|
138
146
|
onPressRef.current = onPress;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
return React.useCallback(onPressRef.current, []);
|
|
149
|
+
return React.useCallback(onPressRef.current, [item]);
|
|
142
150
|
};
|
package/reactHooks/feed/index.ts
CHANGED
|
@@ -6,7 +6,11 @@ export { useEntryScreenId } from "./useEntryScreenId";
|
|
|
6
6
|
|
|
7
7
|
export { useBuildPipesUrl } from "./useBuildPipesUrl";
|
|
8
8
|
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
useMarkPipesDataStale,
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
12
|
+
usePipesCacheReset,
|
|
13
|
+
} from "./useMarkPipesDataStale";
|
|
10
14
|
|
|
11
15
|
export { useBatchLoading } from "./useBatchLoading";
|
|
12
16
|
|
|
@@ -159,9 +159,17 @@ export const useBatchLoading = (
|
|
|
159
159
|
options.riverId,
|
|
160
160
|
]);
|
|
161
161
|
|
|
162
|
+
// Initial preload only. Batch loading warms the first batch of component
|
|
163
|
+
// feeds and signals readiness; the feed set is frozen at mount on purpose.
|
|
164
|
+
// Per-component loads and reloads (including stale revalidation) are owned by
|
|
165
|
+
// useFeedLoader in ZappPipesDataConnector. Re-running on `feeds`/input changes
|
|
166
|
+
// would re-fire on every store update and cause redundant dispatch storms, so
|
|
167
|
+
// this intentionally runs once on mount.
|
|
168
|
+
/* eslint-disable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
162
169
|
React.useEffect(() => {
|
|
163
170
|
runBatchLoading();
|
|
164
|
-
}, [
|
|
171
|
+
}, []);
|
|
172
|
+
/* eslint-enable @wogns3623/better-exhaustive-deps/exhaustive-deps */
|
|
165
173
|
|
|
166
174
|
React.useEffect(() => {
|
|
167
175
|
// check if all feeds are ready and set hasEverBeenReady to true
|
|
@@ -2,17 +2,19 @@ import React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { getDatasourceUrl } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/getDatasourceUrl";
|
|
4
4
|
import { usePipesContexts } from "@applicaster/zapp-react-native-ui-components/Decorators/RiverFeedLoader/utils/usePipesContexts";
|
|
5
|
-
import {
|
|
5
|
+
import { markPipesDataStale } from "@applicaster/zapp-react-native-redux/ZappPipes";
|
|
6
6
|
|
|
7
7
|
import { useRoute } from "../navigation";
|
|
8
8
|
import { useAppDispatch } from "@applicaster/zapp-react-native-redux";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Mark a river's `clear_cache_on_reload` feeds as stale when the screen is
|
|
12
|
+
* unmounted. The cached data is kept (so a screen sharing the same feed does
|
|
13
|
+
* not lose it mid-mount) and revalidated on next access.
|
|
12
14
|
* @param {string} riverId screen id
|
|
13
15
|
* @param {Array} riverComponents list of UI components
|
|
14
16
|
*/
|
|
15
|
-
export const
|
|
17
|
+
export const useMarkPipesDataStale = (riverId, riverComponents) => {
|
|
16
18
|
const dispatch = useAppDispatch();
|
|
17
19
|
const { screenData, pathname } = useRoute();
|
|
18
20
|
const pipesContexts = usePipesContexts(riverId, pathname);
|
|
@@ -35,10 +37,16 @@ export const usePipesCacheReset = (riverId, riverComponents) => {
|
|
|
35
37
|
);
|
|
36
38
|
|
|
37
39
|
if (url) {
|
|
38
|
-
dispatch(
|
|
40
|
+
dispatch(markPipesDataStale(url, { riverId }));
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
};
|
|
43
45
|
}, []);
|
|
44
46
|
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated Renamed to `useMarkPipesDataStale`. Kept as an alias for backward
|
|
50
|
+
* compatibility with external consumers.
|
|
51
|
+
*/
|
|
52
|
+
export const usePipesCacheReset = useMarkPipesDataStale;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* This hook returns a previous value that was passed to it.
|
|
@@ -40,6 +40,7 @@ export const shouldDispatchData = (
|
|
|
40
40
|
) => {
|
|
41
41
|
const currentFeedHasData = feed?.data;
|
|
42
42
|
const isLocalFeed = checkIsLocalFeed(url);
|
|
43
|
+
const isFeedStale = feed?.stale;
|
|
43
44
|
|
|
44
|
-
return !currentFeedHasData || clearCache || isLocalFeed;
|
|
45
|
+
return !currentFeedHasData || clearCache || isFeedStale || isLocalFeed;
|
|
45
46
|
};
|
|
@@ -2,6 +2,12 @@ import React from "react";
|
|
|
2
2
|
import { useContentTypes } from "@applicaster/zapp-react-native-redux/hooks";
|
|
3
3
|
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
4
4
|
import { useRivers } from "../../state";
|
|
5
|
+
import { createLogger } from "../../../logger";
|
|
6
|
+
|
|
7
|
+
const { log_warning } = createLogger({
|
|
8
|
+
subsystem: "zapp-react-native-utils/reactHooks/videoModal",
|
|
9
|
+
category: "useVideoModalScreenData",
|
|
10
|
+
});
|
|
5
11
|
|
|
6
12
|
export const useVideoModalScreenData = ():
|
|
7
13
|
| (ZappEntry & { targetScreen: any }) // TODO: fix ZappEntry type ( was ZappRiver but conflict )
|
|
@@ -14,10 +20,22 @@ export const useVideoModalScreenData = ():
|
|
|
14
20
|
const rivers = useRivers();
|
|
15
21
|
|
|
16
22
|
return React.useMemo(() => {
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const itemType = item?.type?.value;
|
|
24
|
+
|
|
25
|
+
if (!itemType) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
const screenId = contentTypes[itemType]?.screen_id;
|
|
30
|
+
|
|
31
|
+
if (!screenId) {
|
|
32
|
+
log_warning(
|
|
33
|
+
`Type mapping is missing for item type: ${itemType}, title: ${item.title}`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return;
|
|
21
37
|
}
|
|
22
|
-
|
|
38
|
+
|
|
39
|
+
return { ...item, targetScreen: rivers?.[screenId] };
|
|
40
|
+
}, [contentTypes, item, rivers]);
|
|
23
41
|
};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { NativeModules,
|
|
2
|
+
import { NativeModules, View, StyleSheet } from "react-native";
|
|
3
3
|
import { getXray } from "@applicaster/zapp-react-native-utils/logger";
|
|
4
4
|
|
|
5
|
-
import { isApplePlatform, isWeb } from "../reactUtils";
|
|
5
|
+
import { isApplePlatform, isWeb, isTV } from "../reactUtils";
|
|
6
6
|
import { useRivers } from "../reactHooks";
|
|
7
7
|
|
|
8
|
+
const isTVPlatform = isTV();
|
|
9
|
+
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
container: isTVPlatform
|
|
12
|
+
? {
|
|
13
|
+
flex: 1,
|
|
14
|
+
}
|
|
15
|
+
: {},
|
|
16
|
+
});
|
|
17
|
+
|
|
8
18
|
const layoutReducer = (state, { payload }) => {
|
|
9
19
|
return state.map((item, index, _state) => ({
|
|
10
20
|
height: index === payload.index ? payload.height : item.height,
|
|
@@ -30,12 +40,6 @@ type MeasurementContext = {
|
|
|
30
40
|
onLayout: (index: number) => (event) => void;
|
|
31
41
|
};
|
|
32
42
|
|
|
33
|
-
const styles = StyleSheet.create({
|
|
34
|
-
container: {
|
|
35
|
-
flex: 1,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
43
|
const { Logger } = getXray();
|
|
40
44
|
|
|
41
45
|
const logger = new Logger("general", "ui");
|
|
@@ -152,9 +156,5 @@ export const ScreenLoadingMeasurementsListItemWrapper = ({
|
|
|
152
156
|
}: ItemProps) => {
|
|
153
157
|
const { onLayout } = React.useContext(MeasurementsSettersContext);
|
|
154
158
|
|
|
155
|
-
return (
|
|
156
|
-
<View style={styles.container} onLayout={onLayout?.(index)}>
|
|
157
|
-
{children}
|
|
158
|
-
</View>
|
|
159
|
-
);
|
|
159
|
+
return <View onLayout={onLayout?.(index)}>{children}</View>;
|
|
160
160
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const usePipesCacheReset = jest.fn();
|