diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..1dfe5c446 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +integration-testing/ +lib/ +node_modules/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..97dc99b30 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,50 @@ +module.exports = { + root: true, + extends: [ + '@react-native', + 'plugin:react/recommended', + 'plugin:react-native/all', + 'prettier', // Disables ESLint rules that conflict with Prettier + ], + rules: { + 'react/react-in-jsx-scope': 'off', + }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + // We need more verbose typing to enable the below rule, but we should + // do this in the future + // 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, + rules: { + '@typescript-eslint/no-var-requires': [ + 'error', + { allow: ['/package\\.json$'] }, + ], + '@typescript-eslint/no-require-imports': [ + 'error', + { allow: ['/package\\.json$'] }, + ], + }, + }, + { + files: ['**/*.test.{js,ts,tsx}', '**/__mocks__/*', '**/__tests__/*'], + plugins: ['jest'], + env: { + jest: true, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-require-imports': 'off', + }, + }, + ], +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..30d1e016e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false +} \ No newline at end of file diff --git a/example/src/components/App/App.utils.tsx b/example/src/components/App/App.utils.tsx index 213ca134d..2dcde5374 100644 --- a/example/src/components/App/App.utils.tsx +++ b/example/src/components/App/App.utils.tsx @@ -1,5 +1,5 @@ import Icon from 'react-native-vector-icons/Ionicons'; -export const getIcon = (name: string, props: Record) => ( +export const getIcon = (name: string, props: Record) => ( ); diff --git a/example/src/components/App/Main.tsx b/example/src/components/App/Main.tsx index e64f5f59a..15a5c1fb5 100644 --- a/example/src/components/App/Main.tsx +++ b/example/src/components/App/Main.tsx @@ -29,9 +29,10 @@ export const Main = () => { useEffect(() => { if (loginInProgress) return; if (isLoggedIn) { - Iterable.inAppManager.getMessages().then((messages) => { - setUnreadMessageCount(messages.length); - }); + Iterable.inAppManager + .getMessages() + .then((messages) => setUnreadMessageCount(messages.length)) + .catch((error) => console.error('Failed to get messages:', error)); } else { // Reset unread message count when user logs out setUnreadMessageCount(0); @@ -42,9 +43,9 @@ export const Main = () => { <> { - const iconName = routeIcon[route.name as keyof typeof routeIcon]; + const iconName = routeIcon[route.name]; return { - tabBarIcon: (props) => getIcon(iconName as string, props), + tabBarIcon: (props) => getIcon(iconName, props), tabBarActiveTintColor: colors.brandPurple, tabBarInactiveTintColor: colors.textSecondary, headerShown: false, diff --git a/example/src/components/Commerce/Commerce.constants.ts b/example/src/components/Commerce/Commerce.constants.ts index 21f8677b6..b7cffc4dd 100644 --- a/example/src/components/Commerce/Commerce.constants.ts +++ b/example/src/components/Commerce/Commerce.constants.ts @@ -1,7 +1,9 @@ +import type { ImageProps } from 'react-native'; + export type CommerceItem = { id: string; name: string; - icon: any; + icon: ImageProps['source']; subtitle: string; price: number; }; @@ -10,6 +12,7 @@ export const items: CommerceItem[] = [ { id: 'black', name: 'Black Coffee', + // eslint-disable-next-line @typescript-eslint/no-require-imports icon: require('./img/black-coffee.png'), subtitle: 'Maximize health benefits', price: 2.53, @@ -17,6 +20,7 @@ export const items: CommerceItem[] = [ { id: 'cappuccino', name: 'Cappuccino', + // eslint-disable-next-line @typescript-eslint/no-require-imports icon: require('./img/cappuccino.png'), subtitle: 'Tasty and creamy', price: 3.56, @@ -24,6 +28,7 @@ export const items: CommerceItem[] = [ { id: 'mocha', name: 'Mocha', + // eslint-disable-next-line @typescript-eslint/no-require-imports icon: require('./img/mocha.png'), subtitle: 'Indulge yourself', price: 4.98, diff --git a/example/src/components/Commerce/Commerce.styles.ts b/example/src/components/Commerce/Commerce.styles.ts index e0a7e01ff..038744134 100644 --- a/example/src/components/Commerce/Commerce.styles.ts +++ b/example/src/components/Commerce/Commerce.styles.ts @@ -6,62 +6,63 @@ import { container, title, subtitle, + shadows, } from '../../constants'; const styles = StyleSheet.create({ - container, - title: { ...title, textAlign: 'center' }, - subtitle: { ...subtitle, textAlign: 'center' }, + button: { ...button, marginTop: 10, paddingVertical: 5, width: 100 }, + buttonText, cardContainer: { backgroundColor: colors?.white, - borderWidth: 1, - padding: 15, borderColor: colors?.grey5, - shadowColor: 'rgba(0,0,0, .2)', - shadowOffset: { height: 0, width: 0 }, - shadowOpacity: 1, - shadowRadius: 1, - elevation: 10, // Required for Android - width: '100%', + borderWidth: 1, + elevation: shadows.card.elevation, // Required for Android height: 150, marginBottom: 10, - }, - infoContainer: { - display: 'flex', - flexDirection: 'row', - gap: 10, - alignItems: 'center', - }, - imageContainer: { - width: 120, - height: 100, - flex: 0, - }, - textContainer: { - paddingTop: 10, - flex: 1, - marginLeft: 10, + padding: 15, + shadowColor: shadows.card.color, + shadowOffset: shadows.card.offset, + shadowOpacity: shadows.card.opacity, + shadowRadius: shadows.card.radius, + width: '100%', }, cardImage: { height: '100%', - width: '100%', resizeMode: 'contain', + width: '100%', + }, + cardSubtitle: { + color: colors.textSecondary, + fontSize: 14, }, cardTitle: { fontSize: 18, fontWeight: 'bold', }, - cardSubtitle: { - fontSize: 14, - color: colors.textSecondary, + container, + imageContainer: { + flex: 0, + height: 100, + width: 120, + }, + infoContainer: { + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + gap: 10, }, price: { - marginVertical: 5, - fontWeight: 'bold', color: colors.salmon50, + fontWeight: 'bold', + marginVertical: 5, }, - button: { ...button, width: 100, marginTop: 10, paddingVertical: 5 }, - buttonText, + subtitle: { ...subtitle, textAlign: 'center' }, + textContainer: { + flex: 1, + marginLeft: 10, + paddingTop: 10, + }, + title: { ...title, textAlign: 'center' }, }); export default styles; diff --git a/example/src/components/Commerce/Commerce.tsx b/example/src/components/Commerce/Commerce.tsx index 87a24d79e..c840f9199 100644 --- a/example/src/components/Commerce/Commerce.tsx +++ b/example/src/components/Commerce/Commerce.tsx @@ -36,7 +36,8 @@ export const Commerce = () => { Commerce - Purchase will be tracked when "Buy" is clicked. See logs for output. + Purchase will be tracked when "Buy" is clicked. See logs for + output. {items.map((item) => ( diff --git a/example/src/components/Login/Login.styles.ts b/example/src/components/Login/Login.styles.ts index 91584cf7e..ef4e1023e 100644 --- a/example/src/components/Login/Login.styles.ts +++ b/example/src/components/Login/Login.styles.ts @@ -1,10 +1,12 @@ import { StyleSheet, type ViewStyle } from 'react-native'; + import { appName, buttonBlock, buttonDisabled, buttonText, buttonTextDisabled, + colors, container, input, label, @@ -19,22 +21,22 @@ const setButton = (buttonToSet: ViewStyle = {}) => ({ }); export const styles = StyleSheet.create({ - loginScreenContainer: { - ...container, - backgroundColor: 'white', - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - }, - formContainer: { marginTop: 24 }, appName, - title, - subtitle, - input: { ...input, marginBottom: 15 }, button: setButton(), buttonDisabled: setButton(buttonDisabled), buttonText, buttonTextDisabled, + formContainer: { marginTop: 24 }, + input: { ...input, marginBottom: 15 }, label, + loadingContainer: { + flex: 1, + justifyContent: 'center', + }, + loginScreenContainer: { + ...container, + backgroundColor: colors.white, + }, + subtitle, + title, }); diff --git a/example/src/components/User/User.styles.ts b/example/src/components/User/User.styles.ts index 1a0199196..4ab5d0452 100644 --- a/example/src/components/User/User.styles.ts +++ b/example/src/components/User/User.styles.ts @@ -7,14 +7,10 @@ const text: TextStyle = { }; const styles = StyleSheet.create({ - container, appName: appNameSmall, button, buttonText, - secondaryButton: { - ...button, - backgroundColor: 'gray', - }, + container, text, }); diff --git a/example/src/components/User/User.tsx b/example/src/components/User/User.tsx index b18fdf3ae..23f8361a5 100644 --- a/example/src/components/User/User.tsx +++ b/example/src/components/User/User.tsx @@ -21,7 +21,7 @@ export const User = () => { Welcome Iterator Logged in as {loggedInAs} - + Logout diff --git a/example/src/constants/styles/index.ts b/example/src/constants/styles/index.ts index 1ed8340b9..794f9680c 100644 --- a/example/src/constants/styles/index.ts +++ b/example/src/constants/styles/index.ts @@ -1,4 +1,5 @@ export * from './colors'; export * from './containers'; export * from './formElements'; +export * from './shadows'; export * from './typography'; diff --git a/example/src/constants/styles/shadows.ts b/example/src/constants/styles/shadows.ts new file mode 100644 index 000000000..bd6f78e32 --- /dev/null +++ b/example/src/constants/styles/shadows.ts @@ -0,0 +1,9 @@ +export const shadows = { + card: { + color: 'rgba(0,0,0, .2)', + offset: { height: 0, width: 0 }, + opacity: 1, + radius: 1, + elevation: 10, // Required for Android + }, +}; diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 8adf2d281..3908db4c3 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -129,6 +129,7 @@ export const IterableAppProvider: FunctionComponent< for (const route of routeNames) { if (url.includes(route.toLowerCase())) { // TODO: Figure out typing for this + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore navigation.navigate(route); return true; @@ -158,7 +159,7 @@ export const IterableAppProvider: FunctionComponent< } // Initialize app - return Iterable.initialize(key as string, config) + return Iterable.initialize(key, config) .then((isSuccessful) => { setIsInitialized(isSuccessful); diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 4b580cee1..5b5ad8a50 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -27,8 +27,4 @@ export type MainScreenProps = export type RootStackScreenProps = StackScreenProps; -declare global { - namespace ReactNavigation { - interface RootParamList extends RootStackParamList {} - } -} +export type { RootStackParamList as RootParamList }; diff --git a/package.json b/package.json index 9646f269b..880d202c6 100644 --- a/package.json +++ b/package.json @@ -75,10 +75,13 @@ "@types/jest": "^29.5.5", "@types/react": "^18.2.44", "@types/react-native-vector-icons": "^6.4.18", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", "commitlint": "^17.0.2", "del-cli": "^5.1.0", "eslint": "^8.51.0", "eslint-config-prettier": "^9.0.0", + "eslint-plugin-jest": "^28.9.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.7.0", "prettier": "^3.0.3", @@ -129,38 +132,6 @@ } } }, - "eslintConfig": { - "root": true, - "extends": [ - "@react-native", - "prettier" - ], - "rules": { - "react/react-in-jsx-scope": "off", - "prettier/prettier": [ - "error", - { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - } - ] - } - }, - "eslintIgnore": [ - "node_modules/", - "lib/", - "integration-testing/" - ], - "prettier": { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - }, "react-native-builder-bob": { "source": "src", "output": "lib", diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 36c8daa1d..0c099f170 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -5,7 +5,7 @@ export class MockRNIterableAPI { static email?: string; static userId?: string; static token?: string; - static lastPushPayload?: any; + static lastPushPayload?: unknown; static attributionInfo?: IterableAttributionInfo; static messages?: IterableInAppMessage[]; static clickedUrl?: string; @@ -16,7 +16,7 @@ export class MockRNIterableAPI { }); } - static setEmail(email: string, authToken?: string | undefined): void { + static setEmail(email: string, authToken?: string): void { MockRNIterableAPI.email = email; MockRNIterableAPI.token = authToken; } @@ -27,7 +27,7 @@ export class MockRNIterableAPI { }); } - static setUserId(userId: string, authToken?: string | undefined): void { + static setUserId(userId: string, authToken?: string): void { MockRNIterableAPI.userId = userId; MockRNIterableAPI.token = authToken; } @@ -48,7 +48,7 @@ export class MockRNIterableAPI { static trackEvent = jest.fn(); - static async getLastPushPayload(): Promise { + static async getLastPushPayload(): Promise { return await new Promise((resolve) => { resolve(MockRNIterableAPI.lastPushPayload); }); diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index e2bc169ac..0227893b9 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -215,7 +215,7 @@ export class Iterable { * Parameters: none */ - static getLastPushPayload(): Promise { + static getLastPushPayload(): Promise { Iterable.logger.log('getLastPushPayload'); return RNIterableAPI.getLastPushPayload(); @@ -236,17 +236,19 @@ export class Iterable { static getAttributionInfo(): Promise { Iterable.logger.log('getAttributionInfo'); - return RNIterableAPI.getAttributionInfo().then((dict: any | undefined) => { - if (dict) { - return new IterableAttributionInfo( - dict.campaignId as number, - dict.templateId as number, - dict.messageId as string - ); - } else { - return undefined; + return RNIterableAPI.getAttributionInfo().then( + (dict?: IterableAttributionInfo) => { + if (dict) { + return new IterableAttributionInfo( + dict.campaignId, + dict.templateId, + dict.messageId + ); + } else { + return undefined; + } } - }); + ); } /** @@ -275,7 +277,7 @@ export class Iterable { * @param {number} templateId the ID of the template to associate with the push open * @param {string} messageId the ID of the message to associate with the push open * @param {boolean} appAlreadyRunning whether or not the app was already running when the push notification arrived - * @param {any | undefined} dataFields information to store with the push open event + * @param {unknown | undefined} dataFields information to store with the push open event */ static trackPushOpenWithCampaignId( @@ -283,7 +285,7 @@ export class Iterable { templateId: number, messageId: string | undefined, appAlreadyRunning: boolean, - dataFields: any | undefined + dataFields?: unknown ) { Iterable.logger.log('trackPushOpenWithCampaignId'); @@ -301,10 +303,10 @@ export class Iterable { * Represent each item in the updateCart event with an IterableCommerceItem object. * See IterableCommerceItem class defined above. * - * @param {Array} items the items added to the shopping cart + * @param {IterableCommerceItem[]} items the items added to the shopping cart */ - static updateCart(items: Array) { + static updateCart(items: IterableCommerceItem[]) { Iterable.logger.log('updateCart'); RNIterableAPI.updateCart(items); @@ -332,14 +334,14 @@ export class Iterable { * Note: total is a parameter that is passed in. Iterable does not sum the price fields of the various items in the purchase event. * * @param {number} total the total cost of the purchase - * @param {Array} items the items included in the purchase + * @param {IterableCommerceItem[]} items the items included in the purchase * @param {any | undefined} dataFields descriptive data to store on the purchase event */ static trackPurchase( total: number, - items: Array, - dataFields: any | undefined + items: IterableCommerceItem[], + dataFields?: unknown ) { Iterable.logger.log('trackPurchase'); @@ -400,7 +402,7 @@ export class Iterable { message: IterableInAppMessage, location: IterableInAppLocation, source: IterableInAppCloseSource, - clickedUrl?: string | undefined + clickedUrl?: string ) { Iterable.logger.log('trackInAppClose'); @@ -439,9 +441,9 @@ export class Iterable { * The eventType is set to "customEvent". * * @param {string} name the eventName of the custom event - * @param {any | undefined} dataFields descriptive data to store on the custom event + * @param {unknown | undefined} dataFields descriptive data to store on the custom event */ - static trackEvent(name: string, dataFields: any | undefined) { + static trackEvent(name: string, dataFields?: unknown) { Iterable.logger.log('trackEvent'); RNIterableAPI.trackEvent(name, dataFields); @@ -458,10 +460,13 @@ export class Iterable { * overwrite their counterparts that already exist on the user's profile. * Otherwise, they are added. * - * @param {any} dataFields data fields to store in user profile + * @param {unknown | undefined} dataFields data fields to store in user profile * @param {boolean} mergeNestedObjects flag indicating whether to merge top-level objects */ - static updateUser(dataFields: any, mergeNestedObjects: boolean) { + static updateUser( + dataFields: unknown | undefined, + mergeNestedObjects: boolean + ) { Iterable.logger.log('updateUser'); RNIterableAPI.updateUser(dataFields, mergeNestedObjects); @@ -480,7 +485,7 @@ export class Iterable { * @param authToken the new auth token (JWT) to set with the new email, optional - if null/undefined, no JWT-related action will be taken */ - static updateEmail(email: string, authToken?: string | undefined) { + static updateEmail(email: string, authToken?: string) { Iterable.logger.log('updateEmail'); RNIterableAPI.updateEmail(email, authToken); @@ -508,19 +513,19 @@ export class Iterable { * pass in null for any of emailListIds, unsubscribedChannelIds, unsubscribedMessageTypeIds, or subscribedMessageTypeIds * to indicate that Iterable should not change the current value on the current user's profile. * - * @param {Array | undefined} emailListIds the list of email lists (by ID) to which the user should be subscribed - * @param {Array | undefined} unsubscribedChannelIds the list of message channels (by ID) to which the user should be unsubscribed - * @param {Array | undefined} unsubscribedMessageTypeIds the list of message types (by ID) to which the user should be unsubscribed (for opt-out message types) - * @param {Array | undefined} subscribedMessageTypeIds the list of message types (by ID) to which the user should be subscribed (for opt-in message types) + * @param {number[] | undefined} emailListIds the list of email lists (by ID) to which the user should be subscribed + * @param {number[] | undefined} unsubscribedChannelIds the list of message channels (by ID) to which the user should be unsubscribed + * @param {number[] | undefined} unsubscribedMessageTypeIds the list of message types (by ID) to which the user should be unsubscribed (for opt-out message types) + * @param {number[] | undefined} subscribedMessageTypeIds the list of message types (by ID) to which the user should be subscribed (for opt-in message types) * @param {number} campaignId the campaign ID to associate with events generated by this request, use -1 if unknown or not applicable * @param {number} templateId the template ID to associate with events generated by this request, use -1 if unknown or not applicable */ static updateSubscriptions( - emailListIds: Array | undefined, - unsubscribedChannelIds: Array | undefined, - unsubscribedMessageTypeIds: Array | undefined, - subscribedMessageTypeIds: Array | undefined, + emailListIds: number[] | undefined, + unsubscribedChannelIds: number[] | undefined, + unsubscribedMessageTypeIds: number[] | undefined, + subscribedMessageTypeIds: number[] | undefined, campaignId: number, templateId: number ) { @@ -579,6 +584,8 @@ export class Iterable { IterableEventName.handleInAppCalled, (messageDict) => { const message = IterableInAppMessage.fromDict(messageDict); + // TODO: Check if we can use chain operator (?.) here instead + const result = Iterable.savedConfig.inAppHandler!(message); RNIterableAPI.setInAppShowResponse(result); } @@ -588,6 +595,8 @@ export class Iterable { if (Iterable.savedConfig.authHandler) { let authResponseCallback: IterableAuthResponseResult; RNEventEmitter.addListener(IterableEventName.handleAuthCalled, () => { + // TODO: Check if we can use chain operator (?.) here instead + Iterable.savedConfig.authHandler!() .then((promiseResult) => { // Promise result can be either just String OR of type AuthResponse. @@ -603,13 +612,13 @@ export class Iterable { authResponseCallback === IterableAuthResponseResult.SUCCESS ) { if ((promiseResult as IterableAuthResponse).successCallback) { - (promiseResult as IterableAuthResponse).successCallback!(); + (promiseResult as IterableAuthResponse).successCallback?.(); } } else if ( authResponseCallback === IterableAuthResponseResult.FAILURE ) { if ((promiseResult as IterableAuthResponse).failureCallback) { - (promiseResult as IterableAuthResponse).failureCallback!(); + (promiseResult as IterableAuthResponse).failureCallback?.(); } } else { Iterable.logger.log('No callback received from native layer'); @@ -617,7 +626,7 @@ export class Iterable { }, 1000); } else if (typeof promiseResult === typeof '') { //If promise only returns string - RNIterableAPI.passAlongAuthToken(promiseResult as String); + RNIterableAPI.passAlongAuthToken(promiseResult as string); } else { Iterable.logger.log( 'Unexpected promise returned. Auth token expects promise of String or AuthResponse type.' @@ -641,10 +650,10 @@ export class Iterable { ); } - function callUrlHandler(url: any, context: IterableActionContext) { + function callUrlHandler(url: string, context: IterableActionContext) { // TODO: Figure out if this is purposeful // eslint-disable-next-line eqeqeq - if (Iterable.savedConfig.urlHandler!(url, context) == false) { + if (Iterable.savedConfig.urlHandler?.(url, context) == false) { Linking.canOpenURL(url) .then((canOpen) => { if (canOpen) { diff --git a/src/core/classes/IterableAction.ts b/src/core/classes/IterableAction.ts index b325487ae..5bac640f6 100644 --- a/src/core/classes/IterableAction.ts +++ b/src/core/classes/IterableAction.ts @@ -13,7 +13,7 @@ export class IterableAction { this.userInput = userInput; } - static fromDict(dict: any): IterableAction { + static fromDict(dict: IterableAction): IterableAction { return new IterableAction(dict.type, dict.data, dict.userInput); } } diff --git a/src/core/classes/IterableActionContext.ts b/src/core/classes/IterableActionContext.ts index 64581e7fd..8eec4ed15 100644 --- a/src/core/classes/IterableActionContext.ts +++ b/src/core/classes/IterableActionContext.ts @@ -13,9 +13,9 @@ export class IterableActionContext { this.source = source; } - static fromDict(dict: any): IterableActionContext { + static fromDict(dict: IterableActionContext): IterableActionContext { const action = IterableAction.fromDict(dict.action); - const source = dict.source as IterableActionSource; + const source = dict.source; return new IterableActionContext(action, source); } } diff --git a/src/core/classes/IterableCommerceItem.ts b/src/core/classes/IterableCommerceItem.ts index 8cbcaad39..3beffad7f 100644 --- a/src/core/classes/IterableCommerceItem.ts +++ b/src/core/classes/IterableCommerceItem.ts @@ -10,8 +10,8 @@ export class IterableCommerceItem { description?: string; url?: string; imageUrl?: string; - categories?: Array; - dataFields?: any; + categories?: string[]; + dataFields?: unknown; constructor( id: string, @@ -22,8 +22,8 @@ export class IterableCommerceItem { description?: string, url?: string, imageUrl?: string, - categories?: Array, - dataFields?: any | undefined + categories?: string[], + dataFields?: unknown ) { this.id = id; this.name = name; diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index be678e3d8..0b3fa0f3f 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -12,7 +12,6 @@ import type { IterableAuthResponse } from './IterableAuthResponse'; * An IterableConfig object sets various properties of the SDK. * An IterableConfig object is passed into the static initialize method on the Iterable class when initializing the SDK. */ - export class IterableConfig { /** * The name of the Iterable push integration that will send push notifications to your app. @@ -39,7 +38,7 @@ export class IterableConfig { * Number of seconds to wait when displaying multiple in-app messages in sequence. * between each. Defaults to 30 seconds. */ - inAppDisplayInterval: number = 30.0; + inAppDisplayInterval = 30.0; /** * A callback function used to handle deep link URLs and in-app message button and link URLs. @@ -69,7 +68,7 @@ export class IterableConfig { * React Native SDK. Provide an implementation for this method only if your app uses a * JWT-enabled API key. */ - authHandler?: () => Promise; + authHandler?: () => Promise; /** * Set the verbosity of Android and iOS project's log system. @@ -82,13 +81,13 @@ export class IterableConfig { * This is for calls within the React Native layer, and is separate from `logLevel` * which affects the Android and iOS native SDKs */ - logReactNativeSdkCalls: boolean = true; + logReactNativeSdkCalls = true; /** * The number of seconds before the current JWT's expiration that the SDK should call the * authHandler to get an updated JWT. */ - expiringAuthTokenRefreshPeriod: number = 60.0; + expiringAuthTokenRefreshPeriod = 60.0; /** * Use this array to declare the specific URL protocols that the SDK can expect to see on incoming @@ -106,13 +105,13 @@ export class IterableConfig { * * This specifies the `useInMemoryStorageForInApps` config option downstream to the Android SDK layer. */ - androidSdkUseInMemoryStorageForInApps: boolean = false; + androidSdkUseInMemoryStorageForInApps = false; /** * This specifies the `useInMemoryStorageForInApps` config option downstream to the native SDK layers. * Please read the respective `IterableConfig` files for specific details on this config option. */ - useInMemoryStorageForInApps: boolean = false; + useInMemoryStorageForInApps = false; /** * This specifies the data region which determines the data center and associated endpoints used by the SDK @@ -130,9 +129,9 @@ export class IterableConfig { * Android only feature: This controls whether the SDK should enforce encryption for all PII stored on disk. * By default, the SDK will not enforce encryption and may fallback to unencrypted storage in case the encryption fails. */ - encryptionEnforced: boolean = false; + encryptionEnforced = false; - toDict(): any { + toDict() { return { pushIntegrationName: this.pushIntegrationName, autoPushRegistration: this.autoPushRegistration, diff --git a/src/core/classes/IterableEdgeInsets.ts b/src/core/classes/IterableEdgeInsets.ts index dd3da9749..f76517720 100644 --- a/src/core/classes/IterableEdgeInsets.ts +++ b/src/core/classes/IterableEdgeInsets.ts @@ -1,3 +1,5 @@ +import type { IterableEdgeInsetDetails } from '../types'; + // TODO: Add description export class IterableEdgeInsets { top: number; @@ -12,12 +14,7 @@ export class IterableEdgeInsets { this.right = right; } - static fromDict(dict: any): IterableEdgeInsets { - return new IterableEdgeInsets( - dict.top as number, - dict.left as number, - dict.bottom as number, - dict.right as number - ); + static fromDict(dict: IterableEdgeInsetDetails): IterableEdgeInsets { + return new IterableEdgeInsets(dict.top, dict.left, dict.bottom, dict.right); } } diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index bc02410b0..a27ebb067 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -8,7 +8,7 @@ export class IterableLogger { this.config = config; } - log(message: String) { + log(message: string) { // default to `true` in the case of unit testing where `Iterable` is not initialized // which is most likely in a debug environment anyways const loggingEnabled = this.config.logReactNativeSdkCalls ?? true; diff --git a/src/core/classes/IterableUtil.ts b/src/core/classes/IterableUtil.ts index b30b2935a..50d01fe03 100644 --- a/src/core/classes/IterableUtil.ts +++ b/src/core/classes/IterableUtil.ts @@ -1,7 +1,7 @@ // TODO: Add a description // TODO: Change to a util function instead of a class export class IterableUtil { - static readBoolean(dict: any, key: string): boolean { + static readBoolean(dict: Record, key: string): boolean { if (dict[key]) { return dict[key] as boolean; } else { diff --git a/src/core/index.ts b/src/core/index.ts index 731284104..250860473 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,4 @@ export * from './classes'; export * from './enums'; export * from './hooks'; +export * from './types'; diff --git a/src/core/types/IterableEdgeInsetDetails.ts b/src/core/types/IterableEdgeInsetDetails.ts new file mode 100644 index 000000000..5915eedd5 --- /dev/null +++ b/src/core/types/IterableEdgeInsetDetails.ts @@ -0,0 +1,6 @@ +export interface IterableEdgeInsetDetails { + top: number; + left: number; + bottom: number; + right: number; +} diff --git a/src/core/types/index.ts b/src/core/types/index.ts new file mode 100644 index 000000000..f5d846482 --- /dev/null +++ b/src/core/types/index.ts @@ -0,0 +1 @@ +export * from './IterableEdgeInsetDetails'; diff --git a/src/inApp/classes/IterableHtmlInAppContent.ts b/src/inApp/classes/IterableHtmlInAppContent.ts index 6e156eec2..ed9c73097 100644 --- a/src/inApp/classes/IterableHtmlInAppContent.ts +++ b/src/inApp/classes/IterableHtmlInAppContent.ts @@ -1,7 +1,10 @@ import { IterableEdgeInsets } from '../../core'; import { IterableInAppContentType } from '../enums'; -import type { IterableInAppContent } from '../types'; +import type { + IterableHtmlInAppContentRaw, + IterableInAppContent, +} from '../types'; // TODO: Add description export class IterableHtmlInAppContent implements IterableInAppContent { @@ -14,10 +17,10 @@ export class IterableHtmlInAppContent implements IterableInAppContent { this.html = html; } - static fromDict(dict: any): IterableHtmlInAppContent { + static fromDict(dict: IterableHtmlInAppContentRaw): IterableHtmlInAppContent { return new IterableHtmlInAppContent( IterableEdgeInsets.fromDict(dict.edgeInsets), - dict.html as string + dict.html ); } } diff --git a/src/inApp/classes/IterableInAppMessage.ts b/src/inApp/classes/IterableInAppMessage.ts index fbf91b1ef..0c752f39e 100644 --- a/src/inApp/classes/IterableInAppMessage.ts +++ b/src/inApp/classes/IterableInAppMessage.ts @@ -47,7 +47,7 @@ export class IterableInAppMessage { /** * Custom Payload for this message. */ - readonly customPayload?: any; + readonly customPayload?: unknown; /** * Whether this inbox message has been read @@ -67,7 +67,7 @@ export class IterableInAppMessage { expiresAt: Date | undefined, saveToInbox: boolean, inboxMetadata: IterableInboxMetadata | undefined, - customPayload: any | undefined, + customPayload: unknown | undefined, read: boolean, priorityLevel: number ) { @@ -84,7 +84,7 @@ export class IterableInAppMessage { } static fromViewToken(viewToken: ViewToken) { - var inAppMessage = viewToken.item.inAppMessage as IterableInAppMessage; + const inAppMessage = viewToken.item.inAppMessage as IterableInAppMessage; return new IterableInAppMessage( inAppMessage.messageId, @@ -108,37 +108,57 @@ export class IterableInAppMessage { ); } - static fromDict(dict: any): IterableInAppMessage { - const messageId = dict.messageId as string; - const campaignId = dict.campaignId as number; + static fromDict(dict: { + messageId: string; + campaignId: number; + trigger: IterableInAppTrigger; + createdAt: number; + expiresAt: number; + saveToInbox: boolean; + inboxMetadata: { + title: string | undefined; + subtitle: string | undefined; + icon: string | undefined; + }; + customPayload: unknown; + read: boolean; + priorityLevel: number; + }): IterableInAppMessage { + const messageId = dict.messageId; + const campaignId = dict.campaignId; const trigger = IterableInAppTrigger.fromDict(dict.trigger); + let createdAt = dict.createdAt; if (createdAt) { - var dateObject = new Date(0); + const dateObject = new Date(0); createdAt = dateObject.setUTCMilliseconds(createdAt); } let expiresAt = dict.expiresAt; if (expiresAt) { - var dateObject = new Date(0); + const dateObject = new Date(0); expiresAt = dateObject.setUTCMilliseconds(expiresAt); } - let saveToInbox = IterableUtil.readBoolean(dict, 'saveToInbox'); - let inboxMetadataDict = dict.inboxMetadata; + const saveToInbox = IterableUtil.readBoolean(dict, 'saveToInbox'); + const inboxMetadataDict = dict.inboxMetadata; let inboxMetadata: IterableInboxMetadata | undefined; if (inboxMetadataDict) { inboxMetadata = IterableInboxMetadata.fromDict(inboxMetadataDict); } else { inboxMetadata = undefined; } - let customPayload = dict.customPayload; - let read = IterableUtil.readBoolean(dict, 'read'); + const customPayload = dict.customPayload; + const read = IterableUtil.readBoolean(dict, 'read'); - let priorityLevel = dict.priorityLevel as number; + const priorityLevel = dict.priorityLevel; return new IterableInAppMessage( messageId, campaignId, trigger, + // TODO: Speak to the team about `IterableInAppMessage` requiring a date + // object, but being passed a number in this case + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore createdAt, expiresAt, saveToInbox, diff --git a/src/inApp/classes/IterableInAppTrigger.ts b/src/inApp/classes/IterableInAppTrigger.ts index 1737bfdf7..daaf3baa9 100644 --- a/src/inApp/classes/IterableInAppTrigger.ts +++ b/src/inApp/classes/IterableInAppTrigger.ts @@ -8,10 +8,9 @@ export class IterableInAppTrigger { this.type = type; } - static fromDict(dict: any): IterableInAppTrigger { - const type = dict.type as - | IterableInAppTriggerType - | IterableInAppTriggerType.immediate; - return new IterableInAppTrigger(type); + static fromDict(dict: { + type: IterableInAppTriggerType; + }): IterableInAppTrigger { + return new IterableInAppTrigger(dict.type); } } diff --git a/src/inApp/classes/IterableInboxMetadata.ts b/src/inApp/classes/IterableInboxMetadata.ts index 517cb7fc8..4bf47fd3e 100644 --- a/src/inApp/classes/IterableInboxMetadata.ts +++ b/src/inApp/classes/IterableInboxMetadata.ts @@ -16,7 +16,11 @@ export class IterableInboxMetadata { this.icon = icon; } - static fromDict(dict: any): IterableInboxMetadata { + static fromDict(dict: { + title: string | undefined; + subtitle: string | undefined; + icon: string | undefined; + }): IterableInboxMetadata { return new IterableInboxMetadata(dict.title, dict.subtitle, dict.icon); } } diff --git a/src/inApp/types/IterableHtmlInAppContentRaw.ts b/src/inApp/types/IterableHtmlInAppContentRaw.ts new file mode 100644 index 000000000..141f4180f --- /dev/null +++ b/src/inApp/types/IterableHtmlInAppContentRaw.ts @@ -0,0 +1,7 @@ +import type { IterableEdgeInsetDetails } from '../../core'; + +/** The raw in-App content details returned from the server */ +export interface IterableHtmlInAppContentRaw { + edgeInsets: IterableEdgeInsetDetails; + html: string; +} diff --git a/src/inApp/types/index.ts b/src/inApp/types/index.ts index bd83b3125..b0b0086f9 100644 --- a/src/inApp/types/index.ts +++ b/src/inApp/types/index.ts @@ -1 +1,2 @@ export * from './IterableInAppContent'; +export * from './IterableHtmlInAppContentRaw'; diff --git a/src/inbox/classes/IterableInboxDataModel.ts b/src/inbox/classes/IterableInboxDataModel.ts index d2456ed9c..98c7d12f3 100644 --- a/src/inbox/classes/IterableInboxDataModel.ts +++ b/src/inbox/classes/IterableInboxDataModel.ts @@ -6,6 +6,7 @@ import { IterableInAppDeleteSource, IterableInAppLocation, IterableInAppMessage, + type IterableHtmlInAppContentRaw, } from '../../inApp'; import type { IterableInboxImpressionRowInfo, @@ -23,8 +24,6 @@ export class IterableInboxDataModel { ) => number; dateMapperFn?: (message: IterableInAppMessage) => string | undefined; - constructor() {} - set( filter?: (message: IterableInAppMessage) => boolean, comparator?: ( @@ -56,7 +55,7 @@ export class IterableInboxDataModel { ); return RNIterableAPI.getHtmlInAppContentForMessage(id).then( - (content: any) => { + (content: IterableHtmlInAppContentRaw) => { return IterableHtmlInAppContent.fromDict(content); } ); @@ -74,9 +73,9 @@ export class IterableInboxDataModel { RNIterableAPI.removeMessage(id, IterableInAppLocation.inbox, deleteSource); } - async refresh(): Promise> { + async refresh(): Promise { return RNIterableAPI.getInboxMessages().then( - (messages: Array) => { + (messages: IterableInAppMessage[]) => { return this.processMessages(messages); }, () => { @@ -87,16 +86,16 @@ export class IterableInboxDataModel { // inbox session tracking functions - startSession(visibleRows: Array = []) { + startSession(visibleRows: IterableInboxImpressionRowInfo[] = []) { RNIterableAPI.startSession(visibleRows); } - async endSession(visibleRows: Array = []) { + async endSession(visibleRows: IterableInboxImpressionRowInfo[] = []) { await this.updateVisibleRows(visibleRows); RNIterableAPI.endSession(); } - updateVisibleRows(visibleRows: Array = []) { + updateVisibleRows(visibleRows: IterableInboxImpressionRowInfo[] = []) { RNIterableAPI.updateVisibleRows(visibleRows); } @@ -106,8 +105,8 @@ export class IterableInboxDataModel { message1: IterableInAppMessage, message2: IterableInAppMessage ) => { - let createdAt1 = message1.createdAt ?? new Date(0); - let createdAt2 = message2.createdAt ?? new Date(0); + const createdAt1 = message1.createdAt ?? new Date(0); + const createdAt2 = message2.createdAt ?? new Date(0); if (createdAt1 < createdAt2) return 1; if (createdAt1 > createdAt2) return -1; @@ -134,16 +133,16 @@ export class IterableInboxDataModel { } private processMessages( - messages: Array - ): Array { + messages: IterableInAppMessage[] + ): IterableInboxRowViewModel[] { return this.sortAndFilter(messages).map( IterableInboxDataModel.getInboxRowViewModelForMessage ); } private sortAndFilter( - messages: Array - ): Array { + messages: IterableInAppMessage[] + ): IterableInAppMessage[] { let sortedFilteredMessages = messages.slice(); // TODO: Figure out if this is purposeful diff --git a/src/inbox/components/IterableInbox.tsx b/src/inbox/components/IterableInbox.tsx index b60caa773..50ce0d959 100644 --- a/src/inbox/components/IterableInbox.tsx +++ b/src/inbox/components/IterableInbox.tsx @@ -26,15 +26,26 @@ import type { } from '../types'; import { IterableInboxEmptyState } from './IterableInboxEmptyState'; import { IterableInboxMessageDisplay } from './IterableInboxMessageDisplay'; -import { IterableInboxMessageList } from './IterableInboxMessageList'; +import { + IterableInboxMessageList, + type IterableInboxMessageListProps, +} from './IterableInboxMessageList'; +import { ITERABLE_INBOX_COLORS } from '../constants'; const RNIterableAPI = NativeModules.RNIterableAPI; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); +const DEFAULT_HEADLINE_HEIGHT = 60; +const ANDROID_HEADLINE_HEIGHT = 70; +const HEADLINE_PADDING_LEFT_PORTRAIT = 30; +const HEADLINE_PADDING_LEFT_LANDSCAPE = 70; + // TODO: Comment -export interface IterableInboxProps { +export interface IterableInboxProps + extends Partial< + Pick + > { returnToInboxTrigger?: boolean; - messageListItemLayout?: Function; customizations?: IterableInboxCustomizations; tabBarHeight?: number; tabBarPadding?: number; @@ -55,7 +66,7 @@ export const IterableInbox = ({ const defaultInboxTitle = 'Inbox'; const inboxDataModel = new IterableInboxDataModel(); - let { height, width, isPortrait } = useDeviceOrientation(); + const { height, width, isPortrait } = useDeviceOrientation(); const appState = useAppStateListener(); const isFocused = useIsFocused(); @@ -65,7 +76,7 @@ export const IterableInbox = ({ IterableInboxRowViewModel[] >([]); const [loading, setLoading] = useState(true); - const [animatedValue] = useState(new Animated.Value(0)); + const [animatedValue] = useState(new Animated.Value(0)); const [isMessageDisplay, setIsMessageDisplay] = useState(false); const [visibleMessageImpressions, setVisibleMessageImpressions] = useState< @@ -73,49 +84,52 @@ export const IterableInbox = ({ >([]); const styles = StyleSheet.create({ - loadingScreen: { - height: '100%', - backgroundColor: 'whitesmoke', - }, - container: { + alignItems: 'center', flex: 1, flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-start', height: '100%', - width: 2 * width, + justifyContent: 'flex-start', paddingBottom: 0, paddingLeft: 0, paddingRight: 0, - }, - - messageListContainer: { - height: '100%', - width: width, - flexDirection: 'column', - justifyContent: 'flex-start', + width: 2 * width, }, headline: { - fontWeight: 'bold', + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, fontSize: 40, - width: '100%', - height: 60, + fontWeight: 'bold', + height: + Platform.OS === 'android' + ? ANDROID_HEADLINE_HEIGHT + : DEFAULT_HEADLINE_HEIGHT, marginTop: 0, - paddingTop: 10, paddingBottom: 10, - paddingLeft: 30, - backgroundColor: 'whitesmoke', + paddingLeft: isPortrait + ? HEADLINE_PADDING_LEFT_PORTRAIT + : HEADLINE_PADDING_LEFT_LANDSCAPE, + paddingTop: 10, + width: '100%', + }, + + loadingScreen: { + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, + height: '100%', }, - }); - let { loadingScreen, container, headline, messageListContainer } = styles; + messageListContainer: { + flexDirection: 'column', + height: '100%', + justifyContent: 'flex-start', + width: width, + }, + }); const navTitleHeight = - headline.height + headline.paddingTop + headline.paddingBottom; - headline = { ...headline, height: Platform.OS === 'android' ? 70 : 60 }; - headline = !isPortrait ? { ...headline, paddingLeft: 70 } : headline; + DEFAULT_HEADLINE_HEIGHT + + styles.headline.paddingTop + + styles.headline.paddingBottom; //fetches inbox messages and adds listener for inbox changes on mount useEffect(() => { @@ -208,7 +222,7 @@ export const IterableInbox = ({ index: number, models: IterableInboxRowViewModel[] ) { - let newRowViewModels = models.map((rowViewModel) => { + const newRowViewModels = models.map((rowViewModel) => { return rowViewModel.inAppMessage.messageId === id ? { ...rowViewModel, read: true } : rowViewModel; @@ -219,6 +233,7 @@ export const IterableInbox = ({ Iterable.trackInAppOpen( // TODO: Have a safety check for models[index].inAppMessage + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore models[index].inAppMessage, IterableInAppLocation.inbox @@ -235,7 +250,7 @@ export const IterableInbox = ({ fetchInboxMessages(); } - function returnToInbox(callback?: Function) { + function returnToInbox(callback?: () => void) { Animated.timing(animatedValue, { toValue: 0, duration: 300, @@ -262,7 +277,7 @@ export const IterableInbox = ({ inAppContentPromise={getHtmlContentForRow( selectedRowViewModel.inAppMessage.messageId )} - returnToInbox={(callback: Function) => returnToInbox(callback)} + returnToInbox={returnToInbox} deleteRow={(messageId: string) => deleteRow(messageId)} contentWidth={width} isPortrait={isPortrait} @@ -272,9 +287,9 @@ export const IterableInbox = ({ function showMessageList(_loading: boolean) { return ( - + {showNavTitle ? ( - + {customizations?.navTitle ? customizations?.navTitle : defaultInboxTitle} @@ -305,7 +320,7 @@ export const IterableInbox = ({ function renderEmptyState() { return loading ? ( - + ) : ( {inboxAnimatedView} + {inboxAnimatedView} ) : ( - {inboxAnimatedView} + {inboxAnimatedView} ); }; diff --git a/src/inbox/components/IterableInboxEmptyState.tsx b/src/inbox/components/IterableInboxEmptyState.tsx index 44b59d6a7..508f65a76 100644 --- a/src/inbox/components/IterableInboxEmptyState.tsx +++ b/src/inbox/components/IterableInboxEmptyState.tsx @@ -1,6 +1,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { type IterableInboxCustomizations } from '../types'; +import { ITERABLE_INBOX_COLORS } from '../constants'; // TODO: Comment export interface IterableInboxEmptyStateProps { @@ -27,44 +28,44 @@ export const IterableInboxEmptyState = ({ const emptyStateTitle = customizations.noMessagesTitle; const emptyStateBody = customizations.noMessagesBody; - let { container, title, body } = styles; - - container = { - ...container, - height: height - navTitleHeight - tabBarHeight - tabBarPadding, - }; - - if (!isPortrait) { - container = { ...container, height: height - navTitleHeight }; - } - return ( - - + + {emptyStateTitle ? emptyStateTitle : defaultTitle} - {emptyStateBody ? emptyStateBody : defaultBody} + + {emptyStateBody ? emptyStateBody : defaultBody} + ); }; const styles = StyleSheet.create({ + body: { + color: ITERABLE_INBOX_COLORS.TEXT, + fontSize: 15, + }, + container: { - height: 0, - backgroundColor: 'whitesmoke', + alignItems: 'center', + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, flexDirection: 'column', + height: 0, justifyContent: 'center', - alignItems: 'center', }, title: { - fontWeight: 'bold', fontSize: 20, + fontWeight: 'bold', paddingBottom: 25, }, - - body: { - fontSize: 15, - color: 'grey', - }, }); diff --git a/src/inbox/components/IterableInboxMessageCell.tsx b/src/inbox/components/IterableInboxMessageCell.tsx index 9b8c00d22..8083bea5d 100644 --- a/src/inbox/components/IterableInboxMessageCell.tsx +++ b/src/inbox/components/IterableInboxMessageCell.tsx @@ -7,15 +7,17 @@ import { Text, TouchableOpacity, View, + type PanResponderGestureState, type TextStyle, type ViewStyle, } from 'react-native'; import { IterableInboxDataModel } from '../classes'; import type { - IterableInboxRowViewModel, IterableInboxCustomizations, + IterableInboxRowViewModel, } from '../types'; +import { ITERABLE_INBOX_COLORS } from '../constants'; // TODO: Change to component function defaultMessageListLayout( @@ -31,87 +33,86 @@ function defaultMessageListLayout( dataModel.getFormattedDate(rowViewModel.inAppMessage) ?? ''; const thumbnailURL = rowViewModel.imageUrl; - let styles = StyleSheet.create({ - unreadIndicatorContainer: { - height: '100%', - flexDirection: 'column', - justifyContent: 'flex-start', + const styles = StyleSheet.create({ + body: { + color: ITERABLE_INBOX_COLORS.TEXT_MUTED, + flexWrap: 'wrap', + fontSize: 15, + paddingBottom: 10, + width: '85%', }, - unreadIndicator: { - width: 15, - height: 15, - borderRadius: 15 / 2, - backgroundColor: 'blue', - marginLeft: 10, - marginRight: 5, - marginTop: 10, + createdAt: { + color: ITERABLE_INBOX_COLORS.TEXT_MUTED, + fontSize: 12, }, - unreadMessageThumbnailContainer: { - paddingLeft: 10, + messageContainer: { flexDirection: 'column', justifyContent: 'center', + paddingLeft: 10, + width: '75%', }, - readMessageThumbnailContainer: { - paddingLeft: 30, - flexDirection: 'column', - justifyContent: 'center', + messageRow: { + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND_LIGHT, + borderColor: ITERABLE_INBOX_COLORS.BORDER, + borderStyle: 'solid', + borderTopWidth: 1, + flexDirection: 'row', + height: 150, + paddingBottom: 10, + paddingTop: 10, + width: '100%', }, - messageContainer: { - paddingLeft: 10, - width: '75%', + readMessageThumbnailContainer: { flexDirection: 'column', justifyContent: 'center', + paddingLeft: 30, }, title: { fontSize: 22, - width: '85%', paddingBottom: 10, + width: '85%', }, - body: { - fontSize: 15, - color: 'lightgray', - width: '85%', - flexWrap: 'wrap', - paddingBottom: 10, + unreadIndicator: { + backgroundColor: ITERABLE_INBOX_COLORS.UNREAD, + borderRadius: 15 / 2, + height: 15, + marginLeft: 10, + marginRight: 5, + marginTop: 10, + width: 15, }, - createdAt: { - fontSize: 12, - color: 'lightgray', + unreadIndicatorContainer: { + flexDirection: 'column', + height: '100%', + justifyContent: 'flex-start', }, - messageRow: { - flexDirection: 'row', - backgroundColor: 'white', - paddingTop: 10, - paddingBottom: 10, - width: '100%', - height: 150, - borderStyle: 'solid', - borderColor: 'lightgray', - borderTopWidth: 1, + unreadMessageThumbnailContainer: { + flexDirection: 'column', + justifyContent: 'center', + paddingLeft: 10, }, }); const resolvedStyles = { ...styles, ...customizations }; - let { + const { unreadIndicatorContainer, - unreadIndicator, unreadMessageThumbnailContainer, - readMessageThumbnailContainer, - messageContainer, title, body, createdAt, messageRow, } = resolvedStyles; + let { unreadIndicator, readMessageThumbnailContainer, messageContainer } = + resolvedStyles; unreadIndicator = !isPortrait ? { ...unreadIndicator, marginLeft: 40 } @@ -168,10 +169,25 @@ export interface IterableInboxMessageCellProps { dataModel: IterableInboxDataModel; rowViewModel: IterableInboxRowViewModel; customizations: IterableInboxCustomizations; - swipingCheck: Function; - messageListItemLayout: Function; - deleteRow: Function; - handleMessageSelect: Function; + swipingCheck: ( + /** Should swiping be enabled? */ + swiping: boolean + ) => void; + messageListItemLayout: ( + /** Is this the last message in the list? */ + isLast: boolean, + rowViewModel: IterableInboxRowViewModel + ) => [React.ReactNode, number] | undefined | null; + deleteRow: ( + /** The ID of the message to delete */ + messageId: string + ) => void; + handleMessageSelect: ( + /** The ID of the message to select */ + messageId: string, + /** The index of the message to select */ + index: number + ) => void; contentWidth: number; isPortrait: boolean; } @@ -197,45 +213,41 @@ export const IterableInboxMessageCell = ({ : 150; if (messageListItemLayout(last, rowViewModel)) { - deleteSliderHeight = messageListItemLayout(last, rowViewModel)[1]; + deleteSliderHeight = + messageListItemLayout(last, rowViewModel)?.[1] ?? deleteSliderHeight; } const styles = StyleSheet.create({ - textContainer: { - width: '100%', - elevation: 2, - }, - deleteSlider: { - flexDirection: 'row', alignItems: 'center', + backgroundColor: ITERABLE_INBOX_COLORS.DESTRUCTIVE, + elevation: 1, + flexDirection: 'row', + height: deleteSliderHeight, justifyContent: 'flex-end', paddingRight: 10, - backgroundColor: 'red', position: 'absolute', - elevation: 1, width: '100%', - height: deleteSliderHeight, + ...(isPortrait ? {} : { paddingRight: 40 }), + }, + + textContainer: { + elevation: 2, + width: '100%', }, textStyle: { - fontWeight: 'bold', + color: ITERABLE_INBOX_COLORS.TEXT_INVERSE, fontSize: 15, - color: 'white', + fontWeight: 'bold', }, }); - let { textContainer, deleteSlider, textStyle } = styles; - - deleteSlider = isPortrait - ? deleteSlider - : { ...deleteSlider, paddingRight: 40 }; - const scrollThreshold = contentWidth / 15; const FORCING_DURATION = 350; //If user swipes, either complete swipe or reset - function userSwipedLeft(gesture: any) { + function userSwipedLeft(gesture: PanResponderGestureState) { if (gesture.dx < -0.6 * contentWidth) { completeSwipe(); } else { @@ -301,11 +313,11 @@ export const IterableInboxMessageCell = ({ return ( <> - - DELETE + + DELETE {messageListItemLayout(last, rowViewModel) - ? messageListItemLayout(last, rowViewModel)[0] + ? messageListItemLayout(last, rowViewModel)?.[0] : defaultMessageListLayout( last, dataModel, diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 95f5256b5..3b6216991 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -8,7 +8,7 @@ import { View, } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; -import { WebView } from 'react-native-webview'; +import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { Iterable, @@ -24,13 +24,20 @@ import { } from '../../inApp'; import { type IterableInboxRowViewModel } from '../types'; +import { ITERABLE_INBOX_COLORS } from '../constants'; // TODO: Comment export interface IterableInboxMessageDisplayProps { rowViewModel: IterableInboxRowViewModel; inAppContentPromise: Promise; - returnToInbox: Function; - deleteRow: Function; + returnToInbox: ( + /** Callback to be executed after returning to the inbox */ + callback?: () => void + ) => void; + deleteRow: ( + /** Id of the row to be deleted */ + id: string + ) => void; contentWidth: number; isPortrait: boolean; } @@ -50,12 +57,8 @@ export const IterableInboxMessageDisplay = ({ ); const styles = StyleSheet.create({ - messageDisplayContainer: { - height: '100%', - width: contentWidth, - backgroundColor: 'whitesmoke', - flexDirection: 'column', - justifyContent: 'flex-start', + contentContainer: { + flex: 1, }, header: { @@ -64,74 +67,63 @@ export const IterableInboxMessageDisplay = ({ width: '100%', }, - returnButtonContainer: { - flexDirection: 'row', + messageDisplayContainer: { + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, + flexDirection: 'column', + height: '100%', justifyContent: 'flex-start', - alignItems: 'center', - width: '25%', - marginLeft: 0, - marginTop: 0, + width: contentWidth, }, - returnButton: { - flexDirection: 'row', + messageTitle: { alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + width: 0.5 * contentWidth, }, - returnButtonIcon: { - color: 'deepskyblue', - fontSize: 40, - paddingLeft: 0, + messageTitleContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'flex-start', + marginTop: 0, + width: '75%', }, - returnButtonText: { - color: 'deepskyblue', + messageTitleText: { + backgroundColor: ITERABLE_INBOX_COLORS.CONTAINER_BACKGROUND, fontSize: 20, + fontWeight: 'bold', }, - messageTitleContainer: { - flexDirection: 'row', - justifyContent: 'flex-start', + returnButton: { alignItems: 'center', - width: '75%', - marginTop: 0, + flexDirection: 'row', }, - messageTitle: { - flexDirection: 'row', - justifyContent: 'center', + returnButtonContainer: { alignItems: 'center', - width: 0.5 * contentWidth, + flexDirection: 'row', + justifyContent: 'flex-start', + marginLeft: 0, + marginTop: 0, + width: '25%', + ...(isPortrait ? {} : { marginLeft: 80 }), }, - messageTitleText: { - fontWeight: 'bold', - fontSize: 20, - backgroundColor: 'whitesmoke', + returnButtonIcon: { + color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, + fontSize: 40, + paddingLeft: 0, }, - contentContainer: { - flex: 1, + returnButtonText: { + color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, + fontSize: 20, }, }); - let { - header, - returnButtonContainer, - returnButton, - returnButtonIcon, - returnButtonText, - messageTitleContainer, - messageTitleText, - messageDisplayContainer, - } = styles; - - // orientation dependent styling - returnButtonContainer = !isPortrait - ? { ...returnButtonContainer, marginLeft: 80 } - : returnButtonContainer; - - let JS = ` + const JS = ` const links = document.querySelectorAll('a') links.forEach(link => { @@ -157,12 +149,12 @@ export const IterableInboxMessageDisplay = ({ }; }); - function handleInAppLinkAction(event: any) { - let URL = event.nativeEvent.data; + function handleInAppLinkAction(event: WebViewMessageEvent) { + const URL = event.nativeEvent.data; - let action = new IterableAction('openUrl', URL, ''); - let source = IterableActionSource.inApp; - let context = new IterableActionContext(action, source); + const action = new IterableAction('openUrl', URL, ''); + const source = IterableActionSource.inApp; + const context = new IterableActionContext(action, source); Iterable.trackInAppClick( rowViewModel.inAppMessage, @@ -204,9 +196,9 @@ export const IterableInboxMessageDisplay = ({ } return ( - - - + + + { returnToInbox(); @@ -217,18 +209,21 @@ export const IterableInboxMessageDisplay = ({ ); }} > - - - Inbox + + + Inbox - + {messageTitle} diff --git a/src/inbox/components/IterableInboxMessageList.tsx b/src/inbox/components/IterableInboxMessageList.tsx index 5baa39709..e5a19e98a 100644 --- a/src/inbox/components/IterableInboxMessageList.tsx +++ b/src/inbox/components/IterableInboxMessageList.tsx @@ -9,17 +9,24 @@ import type { IterableInboxRowViewModel, IterableInboxCustomizations, } from '../types'; -import { IterableInboxMessageCell } from './IterableInboxMessageCell'; +import { + IterableInboxMessageCell, + type IterableInboxMessageCellProps, +} from './IterableInboxMessageCell'; // TODO: Comment -export interface IterableInboxMessageListProps { +export interface IterableInboxMessageListProps + extends Pick< + IterableInboxMessageCellProps, + 'deleteRow' | 'handleMessageSelect' | 'messageListItemLayout' + > { dataModel: IterableInboxDataModel; rowViewModels: IterableInboxRowViewModel[]; customizations: IterableInboxCustomizations; - messageListItemLayout: Function; - deleteRow: Function; - handleMessageSelect: Function; - updateVisibleMessageImpressions: Function; + updateVisibleMessageImpressions: ( + /** Impression details for the rows to be updated */ + rowInfos: IterableInboxImpressionRowInfo[] + ) => void; contentWidth: number; isPortrait: boolean; } @@ -54,7 +61,7 @@ export const IterableInboxMessageList = ({ customizations={customizations} swipingCheck={setSwiping} messageListItemLayout={messageListItemLayout} - deleteRow={(messageId: string) => deleteRow(messageId)} + deleteRow={deleteRow} handleMessageSelect={handleMessageSelect} contentWidth={contentWidth} isPortrait={isPortrait} diff --git a/src/inbox/constants/colors.ts b/src/inbox/constants/colors.ts new file mode 100644 index 000000000..3821860c0 --- /dev/null +++ b/src/inbox/constants/colors.ts @@ -0,0 +1,11 @@ +export const ITERABLE_INBOX_COLORS = { + CONTAINER_BACKGROUND: 'whitesmoke', + CONTAINER_BACKGROUND_LIGHT: 'white', + BUTTON_PRIMARY_TEXT: 'deepskyblue', + DESTRUCTIVE: 'red', + TEXT: 'grey', + TEXT_INVERSE: 'white', + TEXT_MUTED: 'lightgray', + BORDER: 'lightgray', + UNREAD: 'blue', +}; diff --git a/src/inbox/constants/index.ts b/src/inbox/constants/index.ts new file mode 100644 index 000000000..1bae1c0e4 --- /dev/null +++ b/src/inbox/constants/index.ts @@ -0,0 +1 @@ +export * from './colors'; diff --git a/src/inbox/index.ts b/src/inbox/index.ts index e8ff3a69b..14b66f702 100644 --- a/src/inbox/index.ts +++ b/src/inbox/index.ts @@ -1,3 +1,4 @@ export * from './classes'; export * from './components'; +export * from './constants'; export * from './types'; diff --git a/yarn.lock b/yarn.lock index 342f94f80..22c2ebca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2869,6 +2869,24 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.4.0": + version: 4.4.1 + resolution: "@eslint-community/eslint-utils@npm:4.4.1" + dependencies: + eslint-visitor-keys: ^3.4.3 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: a7ffc838eb6a9ef594cda348458ccf38f34439ac77dc090fa1c120024bcd4eb911dfd74d5ef44d42063e7949fa7c5123ce714a015c4abb917d4124be1bd32bfe + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.10.0": + version: 4.12.1 + resolution: "@eslint-community/regexpp@npm:4.12.1" + checksum: 0d628680e204bc316d545b4993d3658427ca404ae646ce541fcc65306b8c712c340e5e573e30fb9f85f4855c0c5f6dca9868931f2fcced06417fbe1a0c6cd2d6 + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.4.0, @eslint-community/regexpp@npm:^4.6.1": version: 4.11.1 resolution: "@eslint-community/regexpp@npm:4.11.1" @@ -3046,10 +3064,13 @@ __metadata: "@types/jest": ^29.5.5 "@types/react": ^18.2.44 "@types/react-native-vector-icons": ^6.4.18 + "@typescript-eslint/eslint-plugin": ^8.13.0 + "@typescript-eslint/parser": ^8.13.0 commitlint: ^17.0.2 del-cli: ^5.1.0 eslint: ^8.51.0 eslint-config-prettier: ^9.0.0 + eslint-plugin-jest: ^28.9.0 eslint-plugin-prettier: ^5.0.1 jest: ^29.7.0 prettier: ^3.0.3 @@ -4522,6 +4543,29 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.13.0" + dependencies: + "@eslint-community/regexpp": ^4.10.0 + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/type-utils": 8.13.0 + "@typescript-eslint/utils": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 + graphemer: ^1.4.0 + ignore: ^5.3.1 + natural-compare: ^1.4.0 + ts-api-utils: ^1.3.0 + peerDependencies: + "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 42d5c14abdf97167147f3d753398cf62f44c05ae69615c2630720007a87f70aabe0440de744eb1f95eb72a6f5d3943069d4c2e030789590d7ccf7210b39d9db1 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^5.57.1": version: 5.62.0 resolution: "@typescript-eslint/parser@npm:5.62.0" @@ -4539,6 +4583,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/parser@npm:8.13.0" + dependencies: + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/typescript-estree": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 + debug: ^4.3.4 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5e2d5b2eb5a30c4eeb75ab05975fd793c6d809399c5f000a918747283c760201311b1df85a699fd260a3d7cff1be5f39938d59a1d2f8e92141402bf32b4ad748 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/scope-manager@npm:5.62.0" @@ -4549,6 +4611,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/scope-manager@npm:8.13.0" + dependencies: + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 + checksum: 7c80fddb07b3b4e77f05c3ad8aec9a4dda553638188618bc993352ed2b39a8db464c8f28dad8dfc4d82e06ac793fa83a9983198231a7a4711a0dc6f0955b8ad5 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/type-utils@npm:5.62.0" @@ -4566,6 +4638,21 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/type-utils@npm:8.13.0" + dependencies: + "@typescript-eslint/typescript-estree": 8.13.0 + "@typescript-eslint/utils": 8.13.0 + debug: ^4.3.4 + ts-api-utils: ^1.3.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 98e369a49c4334d8871283f995f010ef38b023f80f922cfef60c21c635cf3a2992ce634613b931de129bb5f4d4939b36025f4cc5aa958bb21fee8eb4d8b78c60 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/types@npm:5.62.0" @@ -4573,6 +4660,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/types@npm:8.13.0" + checksum: 361489858f07cba8a331d360d73b51a174a902612fd7bb212560a4d7dc2bd704daf252debc410b09e92217aedca9076c3b2892ec76bcf83a7e1575a175942c2e + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -4591,6 +4685,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.13.0" + dependencies: + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/visitor-keys": 8.13.0 + debug: ^4.3.4 + fast-glob: ^3.3.2 + is-glob: ^4.0.3 + minimatch: ^9.0.4 + semver: ^7.6.0 + ts-api-utils: ^1.3.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 43d33fa341b44e11f3dcd627ea38ebe4433320e569d4a502e44acb370f3a6f64609cf4f98f874eefc161aa42487e35b6e499e74ec422f3c629c7bba155c3d88a + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.10.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -4609,6 +4722,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:8.13.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0": + version: 8.13.0 + resolution: "@typescript-eslint/utils@npm:8.13.0" + dependencies: + "@eslint-community/eslint-utils": ^4.4.0 + "@typescript-eslint/scope-manager": 8.13.0 + "@typescript-eslint/types": 8.13.0 + "@typescript-eslint/typescript-estree": 8.13.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 6d6ec83c4806aeeba94777bf82230a2cde9bd5aa90969ac73cd2e3ba22eb6b1e4f7d3710dbe13a1a1734857c3cd3e8522bb043a04e85cea583c91618a28cc200 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -4619,6 +4746,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.13.0": + version: 8.13.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.13.0" + dependencies: + "@typescript-eslint/types": 8.13.0 + eslint-visitor-keys: ^3.4.3 + checksum: eeefa461dbf60c967bcc2905bfd80fd6f5d015e8139c7d7a44a46d8ffa9339089a3a0eb937423e3c59aff306c238ed8821bda935db1da28ae063f2ce1deafe08 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -7142,6 +7279,24 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest@npm:^28.9.0": + version: 28.9.0 + resolution: "eslint-plugin-jest@npm:28.9.0" + dependencies: + "@typescript-eslint/utils": ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + jest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 90863fab5f3f2f033d98042b13769dc82504c489506872ae9926a1d2b6bcc25c5dc41105e28643f5eb81943aff1aa1cd4d44ada5c1add0a43f1c7a619adbc3d2 + languageName: node + linkType: hard + "eslint-plugin-prettier@npm:^4.2.1": version: 4.2.1 resolution: "eslint-plugin-prettier@npm:4.2.1" @@ -8478,7 +8633,7 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.5, ignore@npm:^5.2.0, ignore@npm:^5.2.4": +"ignore@npm:^5.0.5, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 2acfd32a573260ea522ea0bfeff880af426d68f6831f973129e2ba7363f422923cf53aab62f8369cbf4667c7b25b6f8a3761b34ecdb284ea18e87a5262a865be @@ -12982,7 +13137,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4": +"semver@npm:^7.1.3, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -13819,6 +13974,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.3.0": + version: 1.4.0 + resolution: "ts-api-utils@npm:1.4.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: 477601317dc8a6d961788663ee76984005ed20c70689bd6f807eed2cad258d3731edcc4162d438ce04782ca62a05373ba51e484180fc2a081d8dab2bf693a5af + languageName: node + linkType: hard + "ts-node@npm:^10.8.1": version: 10.9.2 resolution: "ts-node@npm:10.9.2"