Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TW-1307] HypeLab advertising integration #1044

Merged
merged 13 commits into from
Mar 4, 2024
Merged
53 changes: 34 additions & 19 deletions src/components/activity-groups-list/activity-groups-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FlashList, ListRenderItem } from '@shopify/flash-list';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, LayoutChangeEvent, Text, View } from 'react-native';

import { DataPlaceholder } from 'src/components/data-placeholder/data-placeholder';
Expand All @@ -9,7 +9,8 @@ import { emptyFn } from 'src/config/general';
import { useAdTemporaryHiding } from 'src/hooks/use-ad-temporary-hiding.hook';
import { useFakeRefreshControlProps } from 'src/hooks/use-fake-refresh-control-props.hook';
import { useInternalAdsAnalytics } from 'src/hooks/use-internal-ads-analytics.hook';
import { usePartnersPromoLoad } from 'src/hooks/use-partners-promo';
import { useListElementIntersection } from 'src/hooks/use-list-element-intersection.hook';
import { useOutsideOfListIntersection } from 'src/hooks/use-outside-of-list-intersection.hook';
import { ActivityGroup, emptyActivity } from 'src/interfaces/activity.interface';
import { useIsPartnersPromoEnabledSelector } from 'src/store/partners-promotion/partners-promotion-selectors';
import { isTheSameDay, isToday, isYesterday } from 'src/utils/date.utils';
Expand All @@ -26,7 +27,7 @@ const getItemType = (item: ListItem) => (typeof item === 'string' ? 'sectionHead
const ListEmptyComponent = <DataPlaceholder text="No Activity records were found" />;

const AVERAGE_ITEM_HEIGHT = 150;
export const PROMOTION_ID = 'activities-promotion';
const PROMOTION_ID = 'activities-promotion';

interface Props {
activityGroups: ActivityGroup[];
Expand All @@ -43,8 +44,6 @@ export const ActivityGroupsList: FC<Props> = ({
handleUpdate = emptyFn,
pageName
}) => {
usePartnersPromoLoad(PROMOTION_ID);

const styles = useActivityGroupsListStyles();

const partnersPromotionEnabled = useIsPartnersPromoEnabledSelector();
Expand All @@ -55,15 +54,6 @@ export const ActivityGroupsList: FC<Props> = ({
const [promotionErrorOccurred, setPromotionErrorOccurred] = useState(false);
const shouldShowPromotion = partnersPromotionEnabled && !promotionErrorOccurred && !isHiddenTemporarily;

const {
onListScroll,
onInsideScrollAdLayout,
onListLayoutChange,
onOutsideOfScrollAdLayout,
onAdLoad,
resetAdState
} = useInternalAdsAnalytics(pageName);

const keyExtractor = useCallback(
(item: ListItem, index: number) => {
const keyRoot = typeof item === 'string' ? item : item[0].hash;
Expand Down Expand Up @@ -117,17 +107,37 @@ export const ActivityGroupsList: FC<Props> = ({
}, [activityGroups]);
const shouldRenderList = sections.length > 0;

useEffect(() => resetAdState(), [resetAdState, shouldRenderList]);
const adRef = useRef<View>(null);
const separateAdParentRef = useRef<View>(null);

const { onAdLoad, resetAdState, onIsVisible } = useInternalAdsAnalytics(pageName);
const {
onListScroll,
onElementLayoutChange: onListAdLayoutChange,
onListLayoutChange,
onUnmount: onListAdUnmount
} = useListElementIntersection(onIsVisible);
const { onElementOrParentLayout: onSeparateAdOrParentLayout, onUnmount: onSeparateAdUnmount } =
useOutsideOfListIntersection(separateAdParentRef, adRef, onIsVisible);
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
if (shouldRenderList) {
onSeparateAdUnmount();
} else {
onListAdUnmount();
}
resetAdState();
}, [onListAdUnmount, onSeparateAdUnmount, resetAdState, shouldRenderList]);

const handlePromotionLayout = useCallback(
(e: LayoutChangeEvent) => {
if (shouldRenderList) {
onInsideScrollAdLayout(e);
onListAdLayoutChange(e);
} else {
onOutsideOfScrollAdLayout(e);
onSeparateAdOrParentLayout();
}
},
[onInsideScrollAdLayout, onOutsideOfScrollAdLayout, shouldRenderList]
[onListAdLayoutChange, onSeparateAdOrParentLayout, shouldRenderList]
);

const Promotion = useMemo(
Expand All @@ -137,6 +147,7 @@ export const ActivityGroupsList: FC<Props> = ({
id={PROMOTION_ID}
style={styles.promotionItem}
testID={ActivityGroupsListSelectors.promotion}
ref={adRef}
onError={handlePromotionError}
onLoad={onAdLoad}
/>
Expand Down Expand Up @@ -192,7 +203,11 @@ export const ActivityGroupsList: FC<Props> = ({

return (
<>
{shouldShowPromotion && <View style={styles.adContainer}>{Promotion}</View>}
{shouldShowPromotion && (
<View style={styles.adContainer} ref={separateAdParentRef} onLayout={onSeparateAdOrParentLayout}>
{Promotion}
</View>
)}
<View style={styles.emptyListWrapper}>
{loadingEnded ? (
ListEmptyComponent
Expand Down
21 changes: 14 additions & 7 deletions src/components/hypelab-promotion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import { AnalyticsEventCategory } from 'src/utils/analytics/analytics-event.enum
import { useAnalytics } from 'src/utils/analytics/use-analytics.hook';
import { HYPELAB_AD_FRAME_URL, HYPELAB_NATIVE_PLACEMENT_SLUG, HYPELAB_SMALL_PLACEMENT_SLUG } from 'src/utils/env.utils';
import { useTimeout } from 'src/utils/hooks';
import { isDefined } from 'src/utils/is-defined';
import { isString } from 'src/utils/is-string';
import { openUrl } from 'src/utils/linking';

import { useHypelabPromotionStyles } from './styles';

const AD_CONTENT_RELATED_URL_SEARCH_PARAMS = ['campaign_slug', 'creative_set_slug', 'placement_slug'];

export const HypelabPromotion: FC<SingleProviderPromotionProps> = ({
variant,
isVisible,
Expand Down Expand Up @@ -64,10 +67,6 @@ export const HypelabPromotion: FC<SingleProviderPromotionProps> = ({
[adHref, onError]
);

const handleAdFrameError = useCallback(() => {
onError();
}, [onError]);

const handleAdFrameMessage = useCallback(
(e: WebViewMessageEvent) => {
try {
Expand All @@ -78,8 +77,16 @@ export const HypelabPromotion: FC<SingleProviderPromotionProps> = ({
setAdFrameAspectRatio(message.width / message.height);
break;
case AdFrameMessageType.Ready:
const prevAdHrefSearchParams = isDefined(adHref) ? new URL(adHref).searchParams : new URLSearchParams();
const newAdHrefSearchParams = new URL(message.ad.cta_url).searchParams;
setAdHref(message.ad.cta_url);
onReady();
if (
AD_CONTENT_RELATED_URL_SEARCH_PARAMS.some(
paramName => prevAdHrefSearchParams.get(paramName) !== newAdHrefSearchParams.get(paramName)
)
) {
onReady();
}
break;
case AdFrameMessageType.Error:
onError();
Expand Down Expand Up @@ -111,7 +118,7 @@ export const HypelabPromotion: FC<SingleProviderPromotionProps> = ({
<WebView
source={adFrameSource}
containerStyle={styles.imageAdFrame}
onError={handleAdFrameError}
onError={onError}
onMessage={handleAdFrameMessage}
webviewDebuggingEnabled={__DEV__}
scrollEnabled={false}
Expand All @@ -128,7 +135,7 @@ export const HypelabPromotion: FC<SingleProviderPromotionProps> = ({
source={adFrameSource}
containerStyle={styles.textAdFrame}
style={[styles.textAdFrame, { aspectRatio: adFrameAspectRatio }]}
onError={handleAdFrameError}
onError={onError}
onMessage={handleAdFrameMessage}
webviewDebuggingEnabled={__DEV__}
scrollEnabled={false}
Expand Down
72 changes: 31 additions & 41 deletions src/components/optimal-promotion/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import FastImage from 'react-native-fast-image';
import useSWR from 'swr';

import { ImagePromotionView } from 'src/components/image-promotion-view';
import { TextPromotionView } from 'src/components/text-promotion-view';
import { PromotionVariantEnum } from 'src/enums/promotion-variant.enum';
import {
usePartnersPromoErrorSelector,
usePartnersPromoLoadingSelector,
usePartnersPromoSelector
} from 'src/store/partners-promotion/partners-promotion-selectors';
import { useCurrentAccountPkhSelector } from 'src/store/wallet/wallet-selectors';
import { SingleProviderPromotionProps } from 'src/types/promotion';
import { useTimeout } from 'src/utils/hooks';
import { isDefined } from 'src/utils/is-defined';
import { useIsEmptyPromotion } from 'src/utils/optimal.utils';
import { getOptimalPromotion, OptimalPromotionAdType, useIsEmptyPromotion } from 'src/utils/optimal.utils';

import { useOptimalPromotionStyles } from './styles';

export const OptimalPromotion: FC<SingleProviderPromotionProps> = ({
interface SelfRefreshingPromotionProps {
shouldRefreshAd: boolean;
}

export const OptimalPromotion: FC<SingleProviderPromotionProps & SelfRefreshingPromotionProps> = ({
variant,
isVisible,
shouldShowCloseButton,
Expand All @@ -27,49 +27,39 @@ export const OptimalPromotion: FC<SingleProviderPromotionProps> = ({
}) => {
const isImageAd = variant === PromotionVariantEnum.Image;
const styles = useOptimalPromotionStyles();
const promo = usePartnersPromoSelector();
const isLoading = usePartnersPromoLoadingSelector();
const errorFromStore = usePartnersPromoErrorSelector();
const [isImageBroken, setIsImageBroken] = useState(false);
const [wasLoading, setWasLoading] = useState(false);
const [shouldPreventShowingPrevAd, setShouldPreventShowingPrevAd] = useState(true);
const [adViewIsReady, setAdViewIsReady] = useState(isImageAd);
const prevIsLoadingRef = useRef(isLoading);
const promotionIsEmpty = useIsEmptyPromotion(promo);
const apiQueryFailed = (isDefined(errorFromStore) || promotionIsEmpty) && wasLoading;
const adIsNotLikelyToLoad = (isDefined(errorFromStore) || promotionIsEmpty) && !isLoading;
const accountPkh = useCurrentAccountPkhSelector();

useTimeout(
() => {
if (adIsNotLikelyToLoad) {
onError();
}
},
2000,
[adIsNotLikelyToLoad, onError]
const localGetOptimalPromotion = useCallback(
() => getOptimalPromotion(isImageAd ? OptimalPromotionAdType.TwMobile : OptimalPromotionAdType.TwToken, accountPkh),
[accountPkh, isImageAd]
);
const {
data: promo,
error,
isValidating,
mutate
} = useSWR(['optimal-promotion', accountPkh, isImageAd], localGetOptimalPromotion);
const prevIsValidatingRef = useRef(isValidating);

useEffect(() => {
if (wasLoading) {
setShouldPreventShowingPrevAd(false);
}
}, [wasLoading]);
useTimeout(() => setShouldPreventShowingPrevAd(false), 2000, []);
const [isImageBroken, setIsImageBroken] = useState(false);
const [adViewIsReady, setAdViewIsReady] = useState(isImageAd);
const promotionIsEmpty = useIsEmptyPromotion(promo);

useEffect(() => {
if (!isLoading && prevIsLoadingRef.current) {
setWasLoading(true);
if (!isValidating) {
mutate();
}
prevIsLoadingRef.current = isLoading;
}, [isLoading]);
// eslint-disable-next-line react-hooks/exhaustive-deps
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved
}, []);

useEffect(() => {
if (apiQueryFailed) {
if (isDefined(error) || promotionIsEmpty) {
onError();
} else if (!promotionIsEmpty && !shouldPreventShowingPrevAd && adViewIsReady) {
} else if (!promotionIsEmpty && promo && adViewIsReady) {
onReady();
}
}, [apiQueryFailed, onError, onReady, promotionIsEmpty, shouldPreventShowingPrevAd, adViewIsReady]);
prevIsValidatingRef.current = isValidating;
}, [onError, onReady, promotionIsEmpty, adViewIsReady, error, promo, isValidating]);

const onImageError = useCallback(() => {
setIsImageBroken(true);
Expand All @@ -78,7 +68,7 @@ export const OptimalPromotion: FC<SingleProviderPromotionProps> = ({

const handleTextPromotionReady = useCallback(() => setAdViewIsReady(true), []);

if (isDefined(errorFromStore) || promotionIsEmpty || isImageBroken || shouldPreventShowingPrevAd) {
if (isDefined(error) || promotionIsEmpty || isImageBroken || !promo) {
return null;
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/promotion-item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { StyleProp, View, ViewProps, ViewStyle } from 'react-native';
import { ActivityIndicator } from 'src/components/activity-indicator';
import { HypelabPromotion } from 'src/components/hypelab-promotion';
import { OptimalPromotion } from 'src/components/optimal-promotion';
import { PROMO_SYNC_INTERVAL } from 'src/config/fixed-times';
import { isAndroid } from 'src/config/system';
import { PromotionProviderEnum } from 'src/enums/promotion-provider.enum';
import { PromotionVariantEnum } from 'src/enums/promotion-variant.enum';
import { useAdTemporaryHiding } from 'src/hooks/use-ad-temporary-hiding.hook';
import { useAuthorisedInterval } from 'src/hooks/use-authed-interval';
import { TestIdProps } from 'src/interfaces/test-id.props';
import { useIsPartnersPromoEnabledSelector } from 'src/store/partners-promotion/partners-promotion-selectors';

Expand All @@ -17,6 +19,7 @@ import { usePromotionItemStyles } from './styles';
interface Props extends TestIdProps {
id: string;
style?: StyleProp<ViewStyle>;
shouldRefreshAd?: boolean;
shouldShowCloseButton?: boolean;
shouldTryHypelabAd?: boolean;
variant?: PromotionVariantEnum;
Expand All @@ -30,6 +33,7 @@ export const PromotionItem = forwardRef<View, Props>(
{
id,
style,
shouldRefreshAd = false,
shouldShowCloseButton = true,
variant = PromotionVariantEnum.Image,
shouldTryHypelabAd = true,
Expand Down Expand Up @@ -63,6 +67,22 @@ export const PromotionItem = forwardRef<View, Props>(
}
}, [isFocused]);

useAuthorisedInterval(
() => {
if (!shouldRefreshAd || !partnersPromotionEnabled || isHiddenTemporarily) {
return;
}

setAdsState({
currentProvider: PromotionProviderEnum.Optimal,
adError: false,
adIsReady: false
});
},
PROMO_SYNC_INTERVAL,
[partnersPromotionEnabled, isHiddenTemporarily, shouldRefreshAd]
);

const handleAdError = useCallback(() => {
setAdsState(prevState => ({ ...prevState, adError: true }));
onError && onError();
Expand Down Expand Up @@ -113,6 +133,7 @@ export const PromotionItem = forwardRef<View, Props>(
variant={variant}
isVisible={adIsReady}
shouldShowCloseButton={shouldShowCloseButton}
shouldRefreshAd={shouldRefreshAd}
onClose={hidePromotion}
onReady={handleOptimalAdReady}
onError={handleOptimalError}
Expand Down
Loading
Loading