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

Feature: Add remote promo sheet #5140

Merged
merged 49 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
77f66bd
feat: initial work on generalizing promo sheet checks and remote prom…
walmat Oct 19, 2023
79b4f33
Merge branch 'develop' into @matthew/APP-865
walmat Oct 20, 2023
4c6d3c9
more work on remote promo sheets
walmat Oct 20, 2023
082b12d
update queries
walmat Oct 24, 2023
552ab5d
Update arc.graphql to expose promoSheet and promoSheetCollection
walmat Oct 25, 2023
9e1c0eb
update campaign checks and consume getPromoSheet query
walmat Oct 25, 2023
d9c8ee8
write promoSheet and promoSheetCollection queries
walmat Oct 25, 2023
75830f3
add remote promo sheet route and name
walmat Oct 25, 2023
aa68885
add a couple mmkv STORAGE_IDS to control whether or not we show promos
walmat Oct 25, 2023
2da3c89
add remote promo sheets feature flag
walmat Oct 25, 2023
a8eef90
tweak remote promo sheet logic
walmat Oct 25, 2023
f586e4f
fix signing remote images
walmat Oct 25, 2023
f3a0420
more sanity checks and refetch interval
walmat Oct 25, 2023
90ba2ac
add RemotePromoSheetProvider and remove unnecessary campaignChecks
walmat Oct 25, 2023
042422b
tweak checks and add Context/Provider for controlling remote promo sh…
walmat Oct 25, 2023
9b7105c
re-enable firstLaunch and hasViewed checks
walmat Oct 25, 2023
e19ac94
another sanity check
walmat Oct 25, 2023
70e4652
fix hasNonZeroAssetBalance
walmat Oct 25, 2023
2f5b8a6
update check fns
walmat Oct 26, 2023
72e063b
add campaign storage to @storage model
walmat Oct 26, 2023
99146fc
update fns and remove some unused ones
walmat Oct 26, 2023
fc6c525
update arc.graphql query to include priority
walmat Oct 26, 2023
6e41777
update provider and sheet to use @storage
walmat Oct 26, 2023
ceb1325
add priority tag to collection query
walmat Oct 26, 2023
e9853e0
update check for campaign to use @/storage and abstraction of check-f…
walmat Oct 26, 2023
36b574f
adjust asset check fns
walmat Oct 26, 2023
595f7f3
syncronize feature unlocks and campaign checks
walmat Oct 27, 2023
da99524
add notifications promo and cleanup analytic events
walmat Oct 27, 2023
07fb766
add nft offers promo sheet and cleanup priority logic
walmat Oct 27, 2023
fdd6e8a
fix conflicting nft offers asset type with contentful
walmat Oct 27, 2023
afd1d55
replace PromoSheet analytics with v2
walmat Oct 27, 2023
3c838a4
revert graphql arc config change and cleanup local promo sheets
walmat Oct 27, 2023
cff32aa
enable i18n in contentful and pass locale through
walmat Oct 27, 2023
29b6865
enable i18n clientside
walmat Oct 28, 2023
3123fab
update language
walmat Oct 30, 2023
cdbab1b
Merge branch 'develop' into @matthew/APP-865
walmat Nov 8, 2023
3263588
remove unused campaigns folder and uncomment check
walmat Nov 9, 2023
cb77217
remove unused campaigns folder
walmat Nov 9, 2023
f065a2c
fix lint and func name
walmat Nov 9, 2023
374c2e8
pass all locales through localized fields
walmat Nov 9, 2023
001327f
change default colors to hex
walmat Nov 13, 2023
9e32a42
Merge branch 'develop' into @matthew/APP-865
walmat Nov 13, 2023
0189eb9
add specific address for testing preview purposes
walmat Nov 13, 2023
803d0f1
Merge branch 'develop' into @matthew/APP-865
walmat Nov 13, 2023
800dbc5
final touches
walmat Nov 14, 2023
15e25b5
re-add hasShown check
walmat Nov 14, 2023
9140043
add isPreviewing actionFn to bypass hasShown check
walmat Nov 14, 2023
4b49b84
get color from theme if primary/secondary button has that prop
walmat Nov 14, 2023
d666bdc
add network to asset check
walmat Nov 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ import { Playground } from './design-system/playground/Playground';
import { TransactionType } from './entities';
import appEvents from './handlers/appEvents';
import handleDeeplink from './handlers/deeplinks';
import {
runFeatureAndCampaignChecks,
runWalletBackupStatusChecks,
} from './handlers/walletReadyEvents';
import { runWalletBackupStatusChecks } from './handlers/walletReadyEvents';
import {
getCachedProviderForNetwork,
isHardHat,
Expand Down Expand Up @@ -85,6 +82,7 @@ import branch from 'react-native-branch';
import { initializeReservoirClient } from '@/resources/reservoir/client';
import { ReviewPromptAction } from '@/storage/schema';
import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { RemotePromoSheetProvider } from '@/components/remote-promo-sheet/RemotePromoSheetProvider';

if (__DEV__) {
reactNativeDisableYellowBox && LogBox.ignoreAllLogs();
Expand Down Expand Up @@ -187,15 +185,6 @@ class OldApp extends Component {
// Everything we need to do after the wallet is ready goes here
logger.info('✅ Wallet ready!');
runWalletBackupStatusChecks();

InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
if (IS_TESTING === 'true') {
return;
}
runFeatureAndCampaignChecks();
}, 2000);
});
}
}

Expand Down Expand Up @@ -284,13 +273,15 @@ class OldApp extends Component {
<Portal>
<View style={containerStyle}>
{this.state.initialRoute && (
<InitialRouteContext.Provider value={this.state.initialRoute}>
<RoutesComponent
onReady={this.handleSentryNavigationIntegration}
ref={this.handleNavigatorRef}
/>
<PortalConsumer />
</InitialRouteContext.Provider>
<RemotePromoSheetProvider isWalletReady={this.props.walletReady}>
<InitialRouteContext.Provider value={this.state.initialRoute}>
<RoutesComponent
onReady={this.handleSentryNavigationIntegration}
ref={this.handleNavigatorRef}
/>
<PortalConsumer />
</InitialRouteContext.Provider>
</RemotePromoSheetProvider>
)}
<OfflineToast />
</View>
Expand Down
10 changes: 10 additions & 0 deletions src/analytics/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const event = {
appStateChange: 'State change',
analyticsTrackingDisabled: 'analytics_tracking.disabled',
analyticsTrackingEnabled: 'analytics_tracking.enabled',
promoSheetShown: 'promo_sheet.shown',
promoSheetDismissed: 'promo_sheet.dismissed',
swapSubmitted: 'Submitted Swap',
// notification promo sheet was shown
notificationsPromoShown: 'notifications_promo.shown',
Expand Down Expand Up @@ -120,6 +122,14 @@ export type EventProperties = {
inputCurrencySymbol: string;
outputCurrencySymbol: string;
};
[event.promoSheetShown]: {
campaign: string;
time_viewed: number;
};
[event.promoSheetDismissed]: {
campaign: string;
time_viewed: number;
};
[event.notificationsPromoShown]: undefined;
[event.notificationsPromoPermissionsBlocked]: undefined;
[event.notificationsPromoPermissionsGranted]: undefined;
Expand Down
10 changes: 5 additions & 5 deletions src/components/PromoSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import LinearGradient from 'react-native-linear-gradient';
import MaskedView from '@react-native-masked-view/masked-view';
import { SheetActionButton, SheetHandle, SlackSheet } from '@/components/sheet';
import { CampaignKey } from '@/campaigns/campaignChecks';
import { analytics } from '@/analytics';
import { analyticsV2 } from '@/analytics';
import {
AccentColorProvider,
Box,
Expand Down Expand Up @@ -38,7 +38,7 @@ type PromoSheetProps = {
backgroundColor: string;
accentColor: string;
sheetHandleColor?: string;
campaignKey: CampaignKey;
campaignKey: CampaignKey | string;
header: string;
subHeader: string;
primaryButtonProps: SheetActionButtonProps;
Expand Down Expand Up @@ -74,7 +74,7 @@ export function PromoSheet({
() => () => {
if (!activated) {
const timeElapsed = (Date.now() - renderedAt) / 1000;
analytics.track('Dismissed Feature Promo', {
analyticsV2.track(analyticsV2.event.promoSheetDismissed, {
campaign: campaignKey,
time_viewed: timeElapsed,
});
Expand All @@ -86,12 +86,12 @@ export function PromoSheet({
const primaryButtonOnPress = useCallback(() => {
activate();
const timeElapsed = (Date.now() - renderedAt) / 1000;
analytics.track('Activated Feature Promo Action', {
analyticsV2.track(analyticsV2.event.promoSheetShown, {
campaign: campaignKey,
time_viewed: timeElapsed,
});
primaryButtonProps.onPress();
}, [activate, campaignKey, primaryButtonProps.onPress, renderedAt]);
}, [activate, campaignKey, primaryButtonProps, renderedAt]);

// We are not using `isSmallPhone` from `useDimensions` here as we
// want to explicitly set a min height.
Expand Down
176 changes: 176 additions & 0 deletions src/components/remote-promo-sheet/RemotePromoSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { useCallback, useEffect } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import { get } from 'lodash';

import { useNavigation } from '@/navigation/Navigation';
import { PromoSheet } from '@/components/PromoSheet';
import { useTheme } from '@/theme';
import { CampaignCheckResult } from './checkForCampaign';
import { usePromoSheetQuery } from '@/resources/promoSheet/promoSheetQuery';
import { maybeSignUri } from '@/handlers/imgix';
import { campaigns } from '@/storage';
import { delay } from '@/utils/delay';
import { Linking } from 'react-native';
import Routes from '@/navigation/routesNames';
import { Language } from '@/languages';
import { useAccountSettings } from '@/hooks';

const DEFAULT_HEADER_HEIGHT = 285;
const DEFAULT_HEADER_WIDTH = 390;

type RootStackParamList = {
RemotePromoSheet: CampaignCheckResult;
};

const enum ButtonType {
Internal = 'Internal',
External = 'External',
}

export const convertLanguageToLocale = (language: Language) => {
if (language === Language.AR_AR) {
return 'ar';
}

return language.replace('_', '-');
};

export function RemotePromoSheet() {
const { colors } = useTheme();
const { goBack, navigate } = useNavigation();
const { params } = useRoute<
RouteProp<RootStackParamList, 'RemotePromoSheet'>
>();
const { campaignId, campaignKey } = params;
const { language } = useAccountSettings();

useEffect(() => {
return () => {
campaigns.set(['isCurrentlyShown'], false);
};
}, []);

const { data, error } = usePromoSheetQuery(
{
id: campaignId,
locale: convertLanguageToLocale(language as Language),
},
{
enabled: !!campaignId,
}
);

const getButtonForType = (type: ButtonType) => {
switch (type) {
default:
case ButtonType.Internal:
return () => internalNavigation();
case ButtonType.External:
return () => externalNavigation();
}
};

const externalNavigation = useCallback(() => {
Linking.openURL(data?.promoSheet?.primaryButtonProps.props.url);
}, []);

const internalNavigation = useCallback(() => {
goBack();

delay(300).then(() =>
navigate(
(Routes as any)[data?.promoSheet?.primaryButtonProps.props.route],
{
...(data?.promoSheet?.primaryButtonProps.props.options || {}),
}
)
);
}, [goBack, navigate, data?.promoSheet]);

if (!data?.promoSheet || error) {
return null;
}

const {
accentColor: accentColorString,
backgroundColor: backgroundColorString,
sheetHandleColor: sheetHandleColorString,
backgroundImage,
headerImage,
headerImageAspectRatio,
header,
items,
primaryButtonProps,
secondaryButtonProps,
subHeader,
} = data.promoSheet;

const accentColor =
(colors as { [key: string]: any })[accentColorString as string] ??
colors.whiteLabel;

const backgroundColor =
(colors as { [key: string]: any })[backgroundColorString as string] ??
colors.trueBlack;

const sheetHandleColor =
(colors as { [key: string]: any })[sheetHandleColorString as string] ??
colors.trueBlack;

const backgroundSignedImageUrl = backgroundImage?.url
? maybeSignUri(backgroundImage.url)
: undefined;

const headerSignedImageUrl = headerImage?.url
? maybeSignUri(headerImage.url)
: undefined;

return (
<PromoSheet
accentColor={accentColor}
backgroundColor={backgroundColor}
backgroundImage={
backgroundSignedImageUrl ? { uri: backgroundSignedImageUrl } : undefined
}
campaignKey={campaignKey}
headerImage={{ uri: headerSignedImageUrl }}
headerImageAspectRatio={
headerImageAspectRatio ?? DEFAULT_HEADER_WIDTH / DEFAULT_HEADER_HEIGHT
}
sheetHandleColor={sheetHandleColor}
header={header ?? ''}
subHeader={subHeader ?? ''}
primaryButtonProps={{
...primaryButtonProps,
onPress: getButtonForType(data.promoSheet.primaryButtonProps.type),
}}
secondaryButtonProps={{
...secondaryButtonProps,
onPress: goBack,
}}
items={items.map((item: any) => {
if (item.gradient) {
if (item.gradient.includes('.')) {
const gradient = get(colors, item.gradient);

return {
...item,
gradient,
};
}

const gradient =
(colors as { [key: string]: any }).gradients[item.gradient] ??
undefined;

return {
...item,
gradient,
};
}

return item;
})}
/>
);
}
78 changes: 78 additions & 0 deletions src/components/remote-promo-sheet/RemotePromoSheetProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, {
useEffect,
createContext,
PropsWithChildren,
useCallback,
useContext,
} from 'react';
import { IS_TESTING } from 'react-native-dotenv';
import { InteractionManager } from 'react-native';
import { noop } from 'lodash';

import { REMOTE_PROMO_SHEETS, useExperimentalFlag } from '@/config';
import { logger } from '@/logger';
import { campaigns } from '@/storage';
import { checkForCampaign } from '@/components/remote-promo-sheet/checkForCampaign';
import { runFeatureUnlockChecks } from '@/handlers/walletReadyEvents';
import { runLocalCampaignChecks } from './localCampaignChecks';

interface WalletReadyContext {
isWalletReady: boolean;
runChecks: () => void;
}

export const RemotePromoSheetContext = createContext<WalletReadyContext>({
isWalletReady: false,
runChecks: noop,
});

type WalletReadyProvider = PropsWithChildren & WalletReadyContext;

export const RemotePromoSheetProvider = ({
isWalletReady = false,
children,
}: WalletReadyProvider) => {
const remotePromoSheets = useExperimentalFlag(REMOTE_PROMO_SHEETS);

const runChecks = useCallback(async () => {
if (!isWalletReady) return;

InteractionManager.runAfterInteractions(async () => {
setTimeout(async () => {
if (IS_TESTING === 'true') return;

// Stop checking for promo sheets if the exp. flag is toggled off
if (!remotePromoSheets) {
logger.info('Campaigns: remote promo sheets is disabled');
return;
}

const showedFeatureUnlock = await runFeatureUnlockChecks();
if (showedFeatureUnlock) return;

const showedLocalPromo = await runLocalCampaignChecks();
if (showedLocalPromo) return;

checkForCampaign();
}, 2_000);
});
}, [isWalletReady, remotePromoSheets]);

useEffect(() => {
runChecks();

return () => {
campaigns.remove(['lastShownTimestamp']);
campaigns.set(['isCurrentlyShown'], false);
};
}, [runChecks]);

return (
<RemotePromoSheetContext.Provider value={{ isWalletReady, runChecks }}>
{children}
</RemotePromoSheetContext.Provider>
);
};

export const useRemotePromoSheetContext = () =>
useContext(RemotePromoSheetContext);
15 changes: 15 additions & 0 deletions src/components/remote-promo-sheet/check-fns/hasNftOffers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import store from '@/redux/store';
import { fetchNftOffers } from '@/resources/reservoir/nftOffersQuery';

export async function hasNftOffers(): Promise<boolean> {
const { accountAddress } = store.getState().settings;

try {
const data = await fetchNftOffers({ walletAddress: accountAddress });
if (!data?.nftOffers) return false;

return data?.nftOffers?.length > 1;
} catch (e) {
return false;
}
}
Loading
Loading