react-native-test-adx 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +355 -253
  2. package/lib/module/AdaxonSdk.js +51 -44
  3. package/lib/module/components/AdaxonProvider.js +13 -3
  4. package/lib/module/components/InterstitialAd.js +279 -0
  5. package/lib/module/config/ApiConstants.js +3 -1
  6. package/lib/module/config/StorageKeys.js +1 -1
  7. package/lib/module/core/AdaxonClient.js +2 -8
  8. package/lib/module/hooks/useAdaxon.js +1 -3
  9. package/lib/module/platform/app.js +44 -3
  10. package/lib/module/platform/reactNative.js +65 -70
  11. package/lib/module/services/device/DeviceInfoService.js +59 -10
  12. package/lib/module/services/event/EventBuilder.js +1 -8
  13. package/lib/module/services/event/EventService.js +7 -5
  14. package/lib/module/types/AdaxonConfig.js +1 -3
  15. package/lib/typescript/src/AdaxonSdk.d.ts +7 -7
  16. package/lib/typescript/src/components/InterstitialAd.d.ts +10 -0
  17. package/lib/typescript/src/config/ApiConstants.d.ts +2 -1
  18. package/lib/typescript/src/core/AdaxonClient.d.ts +0 -1
  19. package/lib/typescript/src/hooks/useAdaxon.d.ts +1 -3
  20. package/lib/typescript/src/index.d.ts +2 -2
  21. package/lib/typescript/src/platform/app.d.ts +2 -10
  22. package/lib/typescript/src/platform/reactNative.d.ts +1 -0
  23. package/lib/typescript/src/platform/types.d.ts +1 -0
  24. package/lib/typescript/src/services/device/DeviceInfoService.d.ts +11 -1
  25. package/lib/typescript/src/services/event/EventBuilder.d.ts +0 -5
  26. package/lib/typescript/src/types/AdaxonConfig.d.ts +0 -10
  27. package/lib/typescript/src/types/EventTypes.d.ts +0 -4
  28. package/package.json +1 -1
  29. package/lib/module/storage/AnonymousIdService.js +0 -31
  30. package/lib/typescript/src/storage/AnonymousIdService.d.ts +0 -11
@@ -1,18 +1,16 @@
1
1
  "use strict";
2
2
 
3
3
  import { AdaxonClient } from "./core/AdaxonClient.js";
4
- import { MEDIATION_PROVIDER, OFFER_OPENED_EVENT } from "./config/ApiConstants.js";
4
+ import { MEDIATION_PROVIDER, OFFER_OPENED_EVENT, RECOMMENDATION_VIEW_EVENT } from "./config/ApiConstants.js";
5
+ import { dismissInterstitial, presentInterstitial } from "./components/InterstitialAd.js";
5
6
  import { AdaxonSdkError } from "./errors/AdaxonSdkError.js";
6
7
  import { detectReactNativePlatform, reactNativeAppInfoReader, reactNativeDeviceReader, reactNativeNetworkInfoReader, reactNativeNetworkStatusReader, reactNativeUrlOpener } from "./platform/reactNative.js";
7
8
  import { buildAddToCartProperties, buildPurchaseProperties, normalizeEventValues } from "./services/event/normalizeProperties.js";
8
9
  import { AdxEvent } from "./services/event/predefined.js";
9
10
  export class AdaxonSdk {
10
- static sharedInstance = new AdaxonSdk();
11
+ static shared = new AdaxonSdk();
11
12
  client = null;
12
13
  testMode = false;
13
- static get shared() {
14
- return AdaxonSdk.sharedInstance;
15
- }
16
14
  isInitialized() {
17
15
  return this.client !== null;
18
16
  }
@@ -70,58 +68,67 @@ export class AdaxonSdk {
70
68
  async trackCompleteRegistration(params = {}, options = {}) {
71
69
  return this.logEvent(AdxEvent.COMPLETE_REGISTRATION, params, options);
72
70
  }
73
- async loadRecommendations(eventName, properties = {}, listener) {
71
+ async showAd() {
74
72
  try {
75
- const response = await this.requireClient().getRecommendations({
76
- eventName,
77
- properties: {
78
- ...properties,
79
- mediation_provider: MEDIATION_PROVIDER,
80
- test_mode: this.testMode
81
- }
82
- });
83
- listener?.onRecommendationsLoaded(response.offers);
84
- return response.offers;
85
- } catch (error) {
86
- const message = AdaxonSdkError.messageOf(error, 'Failed to load recommendations.');
87
- const code = error instanceof AdaxonSdkError ? error.code : undefined;
88
- listener?.onRecommendationsFailedToLoad(message, code);
89
- throw error;
90
- }
91
- }
92
- async showOffer(offer, listener) {
93
- try {
94
- const client = this.requireClient();
95
- const targetUrl = offer.offer_url;
96
- const canOpen = await reactNativeUrlOpener.canOpenURL(targetUrl);
97
- if (!canOpen) {
98
- throw new AdaxonSdkError('VALIDATION_ERROR', `Cannot open offer URL: ${targetUrl}`);
73
+ const offers = await this.loadRecommendations(RECOMMENDATION_VIEW_EVENT);
74
+ const offer = offers[0];
75
+ if (!offer) {
76
+ console.warn('[Adaxon] No offer available.');
77
+ return;
78
+ }
79
+ if (!this.displayInterstitial(offer)) {
80
+ console.warn('[Adaxon] Interstitial host is not mounted. Wrap the app in AdaxonProvider.');
99
81
  }
100
- await reactNativeUrlOpener.openURL(targetUrl);
101
- await client.track(OFFER_OPENED_EVENT, {
102
- offer_id: offer.offer_id,
103
- offer_name: offer.offer_name,
104
- ad_type: offer.ad_type,
105
- mediation_provider: MEDIATION_PROVIDER
106
- });
107
- listener?.onOfferOpened(offer);
108
82
  } catch (error) {
109
- const message = AdaxonSdkError.messageOf(error, 'Failed to open offer.');
110
- listener?.onOfferFailedToOpen(offer, message);
111
- throw error;
83
+ console.warn('[Adaxon] Failed to show ad.', AdaxonSdkError.messageOf(error, 'Failed to show ad.'));
84
+ if (this.testMode) {
85
+ throw error;
86
+ }
112
87
  }
113
88
  }
114
- setExternalUserId(externalUserId) {
115
- this.requireClient().setExternalUserId(externalUserId);
116
- }
117
89
  isNetworkAvailable() {
118
90
  return this.client?.isNetworkAvailable() ?? false;
119
91
  }
120
92
  destroy() {
93
+ dismissInterstitial();
121
94
  this.client?.destroy();
122
95
  this.client = null;
123
96
  this.testMode = false;
124
97
  }
98
+ async loadRecommendations(eventName, properties = {}) {
99
+ const response = await this.requireClient().getRecommendations({
100
+ eventName,
101
+ properties: {
102
+ ...properties,
103
+ mediation_provider: MEDIATION_PROVIDER,
104
+ test_mode: this.testMode
105
+ }
106
+ });
107
+ return response.offers;
108
+ }
109
+ displayInterstitial(offer) {
110
+ return presentInterstitial({
111
+ offer,
112
+ interact: () => {
113
+ this.openOfferUrl(offer).then(dismissInterstitial).catch(error => {
114
+ console.warn('[Adaxon] Failed to open offer URL.', AdaxonSdkError.messageOf(error, 'Failed to open offer.'));
115
+ });
116
+ }
117
+ });
118
+ }
119
+ async openOfferUrl(offer) {
120
+ const client = this.requireClient();
121
+ if (!(await reactNativeUrlOpener.canOpenURL(offer.offer_url))) {
122
+ throw new AdaxonSdkError('VALIDATION_ERROR', `Cannot open offer URL: ${offer.offer_url}`);
123
+ }
124
+ await reactNativeUrlOpener.openURL(offer.offer_url);
125
+ await client.track(OFFER_OPENED_EVENT, {
126
+ offer_id: offer.offer_id,
127
+ offer_name: offer.offer_name,
128
+ ad_type: offer.ad_type,
129
+ mediation_provider: MEDIATION_PROVIDER
130
+ });
131
+ }
125
132
  requireClient() {
126
133
  if (!this.client) {
127
134
  throw new AdaxonSdkError('INVALID_CONFIG', 'Adaxon SDK is not initialized. Call Adaxon.initialize(...) first.');
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { StyleSheet, View } from "../platform/reactNative.js";
4
5
  import { AdaxonSdk } from "../AdaxonSdk.js";
5
6
  import { AdaxonSdkError } from "../errors/AdaxonSdkError.js";
6
7
  import { AdaxonContext } from "../state/AdaxonContext.js";
7
- import { jsx as _jsx } from "react/jsx-runtime";
8
+ import { InterstitialAd } from "./InterstitialAd.js";
9
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
10
  export function AdaxonProvider({
9
11
  configuration,
10
12
  listener,
@@ -44,6 +46,14 @@ export function AdaxonProvider({
44
46
  }), [initializationError, isInitialized, sdk]);
45
47
  return /*#__PURE__*/_jsx(AdaxonContext.Provider, {
46
48
  value: value,
47
- children: children
49
+ children: /*#__PURE__*/_jsxs(View, {
50
+ style: styles.root,
51
+ children: [children, /*#__PURE__*/_jsx(InterstitialAd, {})]
52
+ })
48
53
  });
49
- }
54
+ }
55
+ const styles = StyleSheet.create({
56
+ root: {
57
+ flex: 1
58
+ }
59
+ });
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { BackHandler, Image, Platform, Pressable, SafeAreaView, StatusBar, StyleSheet, Text, View } from "../platform/reactNative.js";
5
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
6
+ const CLOSE_EDGE_INSET = 12;
7
+ const CLOSE_HIT_SIZE = 44;
8
+ let listener = null;
9
+ export function presentInterstitial(session) {
10
+ if (!listener) {
11
+ return false;
12
+ }
13
+ listener(session);
14
+ return true;
15
+ }
16
+ export function dismissInterstitial() {
17
+ listener?.(null);
18
+ }
19
+ const emptySafeAreaInsets = {
20
+ top: 0,
21
+ right: 0,
22
+ bottom: 0,
23
+ left: 0
24
+ };
25
+ function useEmptySafeAreaInsets() {
26
+ return emptySafeAreaInsets;
27
+ }
28
+ const useSafeAreaInsets = loadUseSafeAreaInsets() ?? useEmptySafeAreaInsets;
29
+ function loadUseSafeAreaInsets() {
30
+ try {
31
+ const module = require('react-native-safe-area-context');
32
+ return typeof module.useSafeAreaInsets === 'function' ? module.useSafeAreaInsets : undefined;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function useTopInset() {
38
+ const safeArea = useSafeAreaInsets();
39
+ if (safeArea.top > 0) {
40
+ return safeArea.top;
41
+ }
42
+ if (Platform.OS === 'android') {
43
+ return StatusBar.currentHeight ?? 0;
44
+ }
45
+ return 0;
46
+ }
47
+ export function InterstitialAd() {
48
+ const [session, setSession] = useState(null);
49
+ const [closeTone, setCloseTone] = useState('onLightSurface');
50
+ const safeArea = useSafeAreaInsets();
51
+ const topInset = useTopInset();
52
+ const offerId = session?.offer.offer_id;
53
+ useEffect(() => {
54
+ listener = setSession;
55
+ return () => {
56
+ if (listener === setSession) {
57
+ listener = null;
58
+ }
59
+ };
60
+ }, []);
61
+ useEffect(() => {
62
+ setCloseTone('onLightSurface');
63
+ }, [offerId]);
64
+ useEffect(() => {
65
+ if (!session) {
66
+ return;
67
+ }
68
+ const onBackPress = () => {
69
+ dismissInterstitial();
70
+ return true;
71
+ };
72
+ const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
73
+ return () => subscription.remove();
74
+ }, [session]);
75
+ if (!session) {
76
+ return null;
77
+ }
78
+ const {
79
+ offer,
80
+ interact
81
+ } = session;
82
+ const closeAppearance = closeButtonAppearance(closeTone);
83
+ const closeRightInset = Platform.OS === 'android' ? CLOSE_EDGE_INSET + safeArea.right : CLOSE_EDGE_INSET;
84
+ const adFrame = /*#__PURE__*/_jsx(_Fragment, {
85
+ children: /*#__PURE__*/_jsxs(View, {
86
+ style: styles.adContainer,
87
+ children: [/*#__PURE__*/_jsx(Pressable, {
88
+ accessibilityRole: "button",
89
+ accessibilityLabel: "Close ad",
90
+ onPress: dismissInterstitial,
91
+ style: [styles.closeButton, closeAppearance.button, {
92
+ top: CLOSE_EDGE_INSET,
93
+ right: closeRightInset
94
+ }],
95
+ testID: "adaxon-interstitial-close",
96
+ children: /*#__PURE__*/_jsx(Text, {
97
+ style: [styles.closeLabel, closeAppearance.label],
98
+ children: "\xD7"
99
+ })
100
+ }), /*#__PURE__*/_jsx(Pressable, {
101
+ accessibilityRole: "button",
102
+ accessibilityLabel: offer.offer_name,
103
+ onPress: interact,
104
+ style: styles.offer,
105
+ testID: "adaxon-interstitial-offer",
106
+ children: /*#__PURE__*/_jsx(OfferCreative, {
107
+ offer: offer,
108
+ onCloseToneChange: setCloseTone
109
+ })
110
+ })]
111
+ })
112
+ });
113
+ return /*#__PURE__*/_jsx(View, {
114
+ style: styles.overlay,
115
+ testID: "adaxon-interstitial",
116
+ children: Platform.OS === 'ios' ? /*#__PURE__*/_jsx(SafeAreaView, {
117
+ style: styles.safeFrame,
118
+ children: adFrame
119
+ }) : /*#__PURE__*/_jsx(View, {
120
+ style: [styles.safeFrame, {
121
+ paddingTop: topInset
122
+ }],
123
+ children: adFrame
124
+ })
125
+ });
126
+ }
127
+ function OfferCreative({
128
+ offer,
129
+ onCloseToneChange
130
+ }) {
131
+ const imageUrl = remoteImageUrl(offer.creative_url) ?? remoteImageUrl(offer.preview_url);
132
+ const [failed, setFailed] = useState(false);
133
+ useEffect(() => {
134
+ setFailed(false);
135
+ }, [imageUrl]);
136
+ useEffect(() => {
137
+ if (!imageUrl || failed) {
138
+ onCloseToneChange('onDarkSurface');
139
+ return;
140
+ }
141
+ onCloseToneChange('onLightSurface');
142
+ }, [failed, imageUrl, onCloseToneChange]);
143
+ if (!imageUrl || failed) {
144
+ return /*#__PURE__*/_jsx(View, {
145
+ style: styles.fallback,
146
+ testID: "adaxon-interstitial-fallback",
147
+ children: /*#__PURE__*/_jsx(Text, {
148
+ style: styles.offerName,
149
+ children: offer.offer_name
150
+ })
151
+ });
152
+ }
153
+ return /*#__PURE__*/_jsx(Image, {
154
+ source: {
155
+ uri: imageUrl
156
+ },
157
+ style: styles.creativeImage,
158
+ resizeMode: "contain",
159
+ onError: () => setFailed(true),
160
+ testID: "adaxon-interstitial-image"
161
+ });
162
+ }
163
+ function closeButtonAppearance(tone) {
164
+ if (tone === 'onDarkSurface') {
165
+ return {
166
+ button: {
167
+ backgroundColor: 'rgba(255, 255, 255, 0.94)',
168
+ borderColor: 'rgba(0, 0, 0, 0.35)'
169
+ },
170
+ label: {
171
+ color: '#111111',
172
+ textShadowColor: 'rgba(255, 255, 255, 0.85)',
173
+ textShadowRadius: 2
174
+ }
175
+ };
176
+ }
177
+ return {
178
+ button: {
179
+ backgroundColor: 'rgba(0, 0, 0, 0.62)',
180
+ borderColor: 'rgba(255, 255, 255, 0.55)'
181
+ },
182
+ label: {
183
+ color: '#ffffff',
184
+ textShadowColor: 'rgba(0, 0, 0, 0.75)',
185
+ textShadowRadius: 3
186
+ }
187
+ };
188
+ }
189
+ function remoteImageUrl(value) {
190
+ const trimmed = value?.trim();
191
+ if (!trimmed) {
192
+ return null;
193
+ }
194
+ try {
195
+ const url = new URL(trimmed);
196
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
197
+ return null;
198
+ }
199
+ return trimmed;
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ const styles = StyleSheet.create({
205
+ overlay: {
206
+ position: 'absolute',
207
+ top: 0,
208
+ right: 0,
209
+ bottom: 0,
210
+ left: 0,
211
+ zIndex: 9999,
212
+ elevation: 9999,
213
+ backgroundColor: '#000000'
214
+ },
215
+ safeFrame: {
216
+ flex: 1,
217
+ width: '100%'
218
+ },
219
+ adContainer: {
220
+ flex: 1,
221
+ width: '100%',
222
+ position: 'relative'
223
+ },
224
+ closeButton: {
225
+ position: 'absolute',
226
+ zIndex: 2,
227
+ minWidth: CLOSE_HIT_SIZE,
228
+ minHeight: CLOSE_HIT_SIZE,
229
+ width: CLOSE_HIT_SIZE,
230
+ height: CLOSE_HIT_SIZE,
231
+ alignItems: 'center',
232
+ justifyContent: 'center',
233
+ borderRadius: CLOSE_HIT_SIZE / 2,
234
+ borderWidth: 1.5,
235
+ shadowColor: '#000000',
236
+ shadowOffset: {
237
+ width: 0,
238
+ height: 2
239
+ },
240
+ shadowOpacity: 0.45,
241
+ shadowRadius: 4,
242
+ elevation: 8
243
+ },
244
+ closeLabel: {
245
+ fontSize: 32,
246
+ lineHeight: 36,
247
+ textShadowOffset: {
248
+ width: 0,
249
+ height: 0
250
+ }
251
+ },
252
+ offer: {
253
+ flex: 1,
254
+ width: '100%',
255
+ alignSelf: 'stretch',
256
+ overflow: 'hidden'
257
+ },
258
+ creativeImage: {
259
+ position: 'absolute',
260
+ top: 0,
261
+ right: 0,
262
+ bottom: 0,
263
+ left: 0,
264
+ width: '100%',
265
+ height: '100%'
266
+ },
267
+ fallback: {
268
+ flex: 1,
269
+ width: '100%',
270
+ alignItems: 'center',
271
+ justifyContent: 'center',
272
+ paddingHorizontal: 24
273
+ },
274
+ offerName: {
275
+ color: '#ffffff',
276
+ fontSize: 22,
277
+ textAlign: 'center'
278
+ }
279
+ });
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
 
3
- export const SDK_VERSION = '0.1.2';
3
+ import packageJson from '../../package.json';
4
+ export const SDK_VERSION = packageJson.version;
4
5
  export const MEDIATION_PROVIDER = 'mts';
5
6
  export const BASE_URL = 'http://localhost:3002';
6
7
  export const EVENT_TRACKING_PATH = '/event/tracking';
7
8
  export const RECOMMENDATIONS_PATH = '/api/mts/recommendations';
8
9
  export const CLICK_TRACKING_PATH = '/click/tracking';
9
10
  export const OFFER_OPENED_EVENT = 'offer_opened';
11
+ export const RECOMMENDATION_VIEW_EVENT = 'recommendation_view';
10
12
  export const DEFAULT_TIMEOUT_MS = 10_000;
11
13
  export const PUBLIC_IP_LOOKUP_URL = 'https://api.ipify.org?format=json';
12
14
  export const PUBLIC_IP_LOOKUP_TIMEOUT_MS = 2_000;
@@ -4,7 +4,7 @@ export const ANONYMOUS_ID_KEY_PREFIX = 'adaxon_sdk.anonymous_id.';
4
4
  export const EVENT_QUEUE_META_KEY_PREFIX = 'adaxon_sdk.event_queue.meta.';
5
5
  export const EVENT_QUEUE_ITEM_KEY_PREFIX = 'adaxon_sdk.event_queue.item.';
6
6
 
7
- // Expo SecureStore and Keychain-backed adapters only accept alphanumerics
7
+ // Queue adapters (and Keychain-backed stores) only accept alphanumerics
8
8
  // plus `.`, `-` and `_`, so app ids are sanitized before use as a key.
9
9
  const UNSAFE_STORAGE_KEY_CHARS = /[^A-Za-z0-9._-]/g;
10
10
  export function sanitizeStorageId(appId) {
@@ -11,7 +11,6 @@ import { EventService } from "../services/event/EventService.js";
11
11
  import { ApiClient } from "../services/network/ApiClient.js";
12
12
  import { NetworkInfoService } from "../services/network/NetworkInfoService.js";
13
13
  import { NetworkService } from "../services/network/NetworkService.js";
14
- import { AnonymousIdService } from "../storage/AnonymousIdService.js";
15
14
  import { EventQueueStorage } from "../storage/EventQueueStorage.js";
16
15
  import { EventValidator } from "../validation/EventValidator.js";
17
16
  export class AdaxonClient {
@@ -27,12 +26,10 @@ export class AdaxonClient {
27
26
  const networkInfo = new NetworkInfoService(config.networkInfoReader);
28
27
  this.builder = new EventBuilder({
29
28
  appId: auth.appId,
30
- deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader),
29
+ deviceInfo: new DeviceInfoService(config.platform ?? 'web', config.deviceInfoReader, config.storage, auth.appId),
31
30
  appInfo: new AppInfoService(Boolean(config.testMode), config.appInfoReader),
32
31
  networkInfo,
33
- anonymousId: new AnonymousIdService(auth.appId, config.storage),
34
- validator: this.validator,
35
- externalUserId: config.externalUserId
32
+ validator: this.validator
36
33
  });
37
34
  this.events = new EventService({
38
35
  api,
@@ -117,9 +114,6 @@ export class AdaxonClient {
117
114
  });
118
115
  return `${BASE_URL}${CLICK_TRACKING_PATH}?${query}`;
119
116
  }
120
- setExternalUserId(externalUserId) {
121
- this.builder.setExternalUserId(externalUserId);
122
- }
123
117
  toQueryString(params) {
124
118
  return Object.entries(params).filter(([, value]) => value !== undefined).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join('&');
125
119
  }
@@ -23,9 +23,7 @@ export function useAdaxon() {
23
23
  trackAddToCart: sdk.trackAddToCart.bind(sdk),
24
24
  trackLogin: sdk.trackLogin.bind(sdk),
25
25
  trackCompleteRegistration: sdk.trackCompleteRegistration.bind(sdk),
26
- loadRecommendations: sdk.loadRecommendations.bind(sdk),
27
- showOffer: sdk.showOffer.bind(sdk),
28
- setExternalUserId: sdk.setExternalUserId.bind(sdk)
26
+ showAd: sdk.showAd.bind(sdk)
29
27
  }), [sdk]);
30
28
  return {
31
29
  isInitialized,
@@ -1,12 +1,53 @@
1
1
  "use strict";
2
2
 
3
3
  import { nonEmptyString, setIfPresent } from "../utils/strings.js";
4
- export function appFieldsFromNative(constants) {
4
+ export function appFieldsFromNative(...sources) {
5
+ const value = key => lastPresent(sources, key);
6
+ const expo = expoConfig(value('expoConfig'), value('manifest'), value('extra'));
5
7
  const fields = {};
6
- setIfPresent(fields, 'package_name', firstNonEmpty(constants.applicationId, constants.bundleId, constants.androidPackage, constants.iosBundleIdentifier));
7
- setIfPresent(fields, 'app_version', firstNonEmpty(constants.nativeApplicationVersion, constants.appVersion, constants.nativeAppVersion));
8
+ setIfPresent(fields, 'package_name', firstNonEmpty(value('applicationId'), value('bundleId'), value('packageName'), value('androidPackage'), value('iosBundleIdentifier'), asRecord(value('android'))?.package, asRecord(value('ios'))?.bundleIdentifier, asRecord(expo?.android)?.package, asRecord(expo?.ios)?.bundleIdentifier));
9
+ setIfPresent(fields, 'app_version', firstNonEmpty(value('nativeApplicationVersion'), value('appVersion'), value('nativeAppVersion'), value('version'), expo?.version));
8
10
  return fields;
9
11
  }
12
+ function expoConfig(expoConfigValue, manifestValue, extraValue) {
13
+ const manifest = asRecord(manifestValue);
14
+ return firstRecord(expoConfigValue, asRecord(manifest?.extra)?.expoClient, manifest?.expoClient, manifest, asRecord(extraValue)?.expoClient);
15
+ }
16
+ function lastPresent(sources, key) {
17
+ let found;
18
+ for (const source of sources) {
19
+ if (!source) {
20
+ continue;
21
+ }
22
+ const next = source[key];
23
+ if (next != null && next !== '') {
24
+ found = next;
25
+ }
26
+ }
27
+ return found;
28
+ }
29
+ function firstRecord(...values) {
30
+ for (const value of values) {
31
+ const record = asRecord(value);
32
+ if (record) {
33
+ return record;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+ function asRecord(value) {
39
+ if (typeof value === 'string') {
40
+ try {
41
+ return asRecord(JSON.parse(value));
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
47
+ return value;
48
+ }
49
+ return undefined;
50
+ }
10
51
  function firstNonEmpty(...values) {
11
52
  for (const value of values) {
12
53
  const next = nonEmptyString(value);