diff --git a/.env.dist b/.env.dist index 971e8b82d8..fa271bceff 100644 --- a/.env.dist +++ b/.env.dist @@ -8,6 +8,7 @@ TEMPLE_WALLET_UTORG_SID= TEMPLE_WALLET_API_URL=https://temple-api-mainnet.prod.templewallet.com TEMPLE_WALLET_DEXES_API_URL=wss://dexes-api-mainnet.prod.templewallet.com +TEMPLE_ADS_API_URL= TEMPLE_WALLET_ROUTE3_AUTH_TOKEN= TEMPLE_WALLET_MOONPAY_API_KEY= @@ -16,17 +17,25 @@ TEMPLE_FIREBASE_CONFIG= TEMPLE_FIREBASE_MESSAGING_VAPID_KEY= HYPELAB_API_URL= +HYPELAB_MISES_SMALL_PLACEMENT_SLUG= HYPELAB_SMALL_PLACEMENT_SLUG= +HYPELAB_MISES_HIGH_PLACEMENT_SLUG= HYPELAB_HIGH_PLACEMENT_SLUG= +HYPELAB_MISES_WIDE_PLACEMENT_SLUG= HYPELAB_WIDE_PLACEMENT_SLUG= +HYPELAB_MISES_NATIVE_PLACEMENT_SLUG= HYPELAB_NATIVE_PLACEMENT_SLUG= HYPELAB_PROPERTY_SLUG= HYPELAB_ADS_WINDOW_URL= PERSONA_ADS_API_KEY= +PERSONA_ADS_MISES_BANNER_UNIT_ID= PERSONA_ADS_BANNER_UNIT_ID= +PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID= PERSONA_ADS_WIDE_BANNER_UNIT_ID= +PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID= PERSONA_ADS_MEDIUM_BANNER_UNIT_ID= +PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID= PERSONA_ADS_SQUARISH_BANNER_UNIT_ID= TEMPLE_ADS_ORIGIN_PASSPHRASE= diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 36f954419e..010c88762f 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -5,6 +5,7 @@ on: branches: - master - development + - 'TW-[0-9]+-epic-**' jobs: pull-request-check: @@ -31,21 +32,30 @@ jobs: TEMPLE_WALLET_UTORG_SID: ${{ secrets.TEMPLE_WALLET_UTORG_SID }} TEMPLE_WALLET_API_URL: ${{ vars.TEMPLE_WALLET_API_URL }} TEMPLE_WALLET_DEXES_API_URL: ${{ vars.TEMPLE_WALLET_DEXES_API_URL }} + TEMPLE_ADS_API_URL: ${{ vars.TEMPLE_ADS_API_URL }} TEMPLE_WALLET_ROUTE3_AUTH_TOKEN: ${{ vars.TEMPLE_WALLET_ROUTE3_AUTH_TOKEN }} TEMPLE_WALLET_MOONPAY_API_KEY: ${{ secrets.TEMPLE_WALLET_MOONPAY_API_KEY }} TEMPLE_FIREBASE_CONFIG: ${{ secrets.TEMPLE_FIREBASE_CONFIG }} TEMPLE_FIREBASE_MESSAGING_VAPID_KEY: ${{ secrets.TEMPLE_FIREBASE_MESSAGING_VAPID_KEY }} HYPELAB_API_URL: ${{ vars.HYPELAB_API_URL }} + HYPELAB_MISES_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_SMALL_PLACEMENT_SLUG }} HYPELAB_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_SMALL_PLACEMENT_SLUG }} + HYPELAB_MISES_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_HIGH_PLACEMENT_SLUG }} HYPELAB_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_HIGH_PLACEMENT_SLUG }} + HYPELAB_MISES_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_WIDE_PLACEMENT_SLUG }} HYPELAB_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_WIDE_PLACEMENT_SLUG }} + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG }} HYPELAB_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_NATIVE_PLACEMENT_SLUG }} HYPELAB_PROPERTY_SLUG: ${{ vars.HYPELAB_PROPERTY_SLUG }} HYPELAB_ADS_WINDOW_URL: ${{ vars.HYPELAB_ADS_WINDOW_URL }} PERSONA_ADS_API_KEY: ${{ vars.PERSONA_ADS_API_KEY }} + PERSONA_ADS_MISES_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_BANNER_UNIT_ID }} PERSONA_ADS_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID }} PERSONA_ADS_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_WIDE_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID }} PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID }} PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID }} TEMPLE_ADS_ORIGIN_PASSPHRASE: ${{ vars.TEMPLE_ADS_ORIGIN_PASSPHRASE }} CONVERSION_VERIFICATION_URL: ${{ vars.CONVERSION_VERIFICATION_URL }} diff --git a/.github/workflows/manual-builds.yml b/.github/workflows/manual-builds.yml index 065bcc843b..668531f126 100644 --- a/.github/workflows/manual-builds.yml +++ b/.github/workflows/manual-builds.yml @@ -50,21 +50,30 @@ jobs: TEMPLE_WALLET_UTORG_SID: ${{ secrets.TEMPLE_WALLET_UTORG_SID }} TEMPLE_WALLET_API_URL: ${{ vars.TEMPLE_WALLET_API_URL }} TEMPLE_WALLET_DEXES_API_URL: ${{ vars.TEMPLE_WALLET_DEXES_API_URL }} + TEMPLE_ADS_API_URL: ${{ vars.TEMPLE_ADS_API_URL }} TEMPLE_WALLET_ROUTE3_AUTH_TOKEN: ${{ vars.TEMPLE_WALLET_ROUTE3_AUTH_TOKEN }} TEMPLE_WALLET_MOONPAY_API_KEY: ${{ secrets.TEMPLE_WALLET_MOONPAY_API_KEY }} TEMPLE_FIREBASE_CONFIG: ${{ secrets.TEMPLE_FIREBASE_CONFIG }} TEMPLE_FIREBASE_MESSAGING_VAPID_KEY: ${{ secrets.TEMPLE_FIREBASE_MESSAGING_VAPID_KEY }} HYPELAB_API_URL: ${{ vars.HYPELAB_API_URL }} + HYPELAB_MISES_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_SMALL_PLACEMENT_SLUG }} HYPELAB_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_SMALL_PLACEMENT_SLUG }} + HYPELAB_MISES_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_HIGH_PLACEMENT_SLUG }} HYPELAB_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_HIGH_PLACEMENT_SLUG }} + HYPELAB_MISES_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_WIDE_PLACEMENT_SLUG }} HYPELAB_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_WIDE_PLACEMENT_SLUG }} + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG }} HYPELAB_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_NATIVE_PLACEMENT_SLUG }} HYPELAB_PROPERTY_SLUG: ${{ vars.HYPELAB_PROPERTY_SLUG }} HYPELAB_ADS_WINDOW_URL: ${{ vars.HYPELAB_ADS_WINDOW_URL }} PERSONA_ADS_API_KEY: ${{ vars.PERSONA_ADS_API_KEY }} + PERSONA_ADS_MISES_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_BANNER_UNIT_ID }} PERSONA_ADS_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID }} PERSONA_ADS_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_WIDE_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID }} PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID }} PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID }} TEMPLE_ADS_ORIGIN_PASSPHRASE: ${{ vars.TEMPLE_ADS_ORIGIN_PASSPHRASE }} CONVERSION_VERIFICATION_URL: ${{ vars.CONVERSION_VERIFICATION_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ffb3dd894..f65de57946 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,21 +32,30 @@ jobs: TEMPLE_WALLET_UTORG_SID: ${{ secrets.TEMPLE_WALLET_UTORG_SID }} TEMPLE_WALLET_API_URL: ${{ vars.TEMPLE_WALLET_API_URL }} TEMPLE_WALLET_DEXES_API_URL: ${{ vars.TEMPLE_WALLET_DEXES_API_URL }} + TEMPLE_ADS_API_URL: ${{ vars.TEMPLE_ADS_API_URL }} TEMPLE_WALLET_ROUTE3_AUTH_TOKEN: ${{ vars.TEMPLE_WALLET_ROUTE3_AUTH_TOKEN }} TEMPLE_WALLET_MOONPAY_API_KEY: ${{ secrets.TEMPLE_WALLET_MOONPAY_API_KEY }} TEMPLE_FIREBASE_CONFIG: ${{ secrets.TEMPLE_FIREBASE_CONFIG }} TEMPLE_FIREBASE_MESSAGING_VAPID_KEY: ${{ secrets.TEMPLE_FIREBASE_MESSAGING_VAPID_KEY }} HYPELAB_API_URL: ${{ vars.HYPELAB_API_URL }} + HYPELAB_MISES_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_SMALL_PLACEMENT_SLUG }} HYPELAB_SMALL_PLACEMENT_SLUG: ${{ vars.HYPELAB_SMALL_PLACEMENT_SLUG }} + HYPELAB_MISES_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_HIGH_PLACEMENT_SLUG }} HYPELAB_HIGH_PLACEMENT_SLUG: ${{ vars.HYPELAB_HIGH_PLACEMENT_SLUG }} + HYPELAB_MISES_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_WIDE_PLACEMENT_SLUG }} HYPELAB_WIDE_PLACEMENT_SLUG: ${{ vars.HYPELAB_WIDE_PLACEMENT_SLUG }} + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG }} HYPELAB_NATIVE_PLACEMENT_SLUG: ${{ vars.HYPELAB_NATIVE_PLACEMENT_SLUG }} HYPELAB_PROPERTY_SLUG: ${{ vars.HYPELAB_PROPERTY_SLUG }} HYPELAB_ADS_WINDOW_URL: ${{ vars.HYPELAB_ADS_WINDOW_URL }} PERSONA_ADS_API_KEY: ${{ vars.PERSONA_ADS_API_KEY }} + PERSONA_ADS_MISES_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_BANNER_UNIT_ID }} PERSONA_ADS_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID }} PERSONA_ADS_WIDE_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_WIDE_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID }} PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID }} PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: ${{ vars.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID }} TEMPLE_ADS_ORIGIN_PASSPHRASE: ${{ vars.TEMPLE_ADS_ORIGIN_PASSPHRASE }} CONVERSION_VERIFICATION_URL: ${{ vars.CONVERSION_VERIFICATION_URL }} diff --git a/.github/workflows/secrets-setup/action.yml b/.github/workflows/secrets-setup/action.yml index a28a360321..4c861f7766 100644 --- a/.github/workflows/secrets-setup/action.yml +++ b/.github/workflows/secrets-setup/action.yml @@ -23,6 +23,8 @@ inputs: required: true TEMPLE_WALLET_DEXES_API_URL: required: true + TEMPLE_ADS_API_URL: + required: true TEMPLE_FIREBASE_CONFIG: required: true TEMPLE_FIREBASE_MESSAGING_VAPID_KEY: @@ -33,12 +35,20 @@ inputs: required: true HYPELAB_API_URL: required: true + HYPELAB_MISES_SMALL_PLACEMENT_SLUG: + required: true HYPELAB_SMALL_PLACEMENT_SLUG: required: true + HYPELAB_MISES_HIGH_PLACEMENT_SLUG: + required: true HYPELAB_HIGH_PLACEMENT_SLUG: required: true + HYPELAB_MISES_WIDE_PLACEMENT_SLUG: + required: true HYPELAB_WIDE_PLACEMENT_SLUG: required: true + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG: + required: true HYPELAB_NATIVE_PLACEMENT_SLUG: required: true HYPELAB_PROPERTY_SLUG: @@ -47,12 +57,20 @@ inputs: required: true PERSONA_ADS_API_KEY: required: true + PERSONA_ADS_MISES_BANNER_UNIT_ID: + required: true PERSONA_ADS_BANNER_UNIT_ID: required: true + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID: + required: true PERSONA_ADS_WIDE_BANNER_UNIT_ID: required: true + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID: + required: true PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: required: true + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID: + required: true PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: required: true CONVERSION_VERIFICATION_URL: @@ -123,23 +141,32 @@ runs: TEMPLE_WALLET_API_URL=${{ inputs.TEMPLE_WALLET_API_URL }} TEMPLE_WALLET_DEXES_API_URL=${{ inputs.TEMPLE_WALLET_DEXES_API_URL }} + TEMPLE_ADS_API_URL=${{ inputs.TEMPLE_ADS_API_URL }} TEMPLE_WALLET_ROUTE3_AUTH_TOKEN=${{ inputs.TEMPLE_WALLET_ROUTE3_AUTH_TOKEN }} TEMPLE_WALLET_MOONPAY_API_KEY=${{ inputs.TEMPLE_WALLET_MOONPAY_API_KEY }} TEMPLE_FIREBASE_CONFIG=${{ inputs.TEMPLE_FIREBASE_CONFIG }} TEMPLE_FIREBASE_MESSAGING_VAPID_KEY=${{ inputs.TEMPLE_FIREBASE_MESSAGING_VAPID_KEY }} HYPELAB_API_URL=${{ inputs.HYPELAB_API_URL }} + HYPELAB_MISES_SMALL_PLACEMENT_SLUG=${{ inputs.HYPELAB_MISES_SMALL_PLACEMENT_SLUG }} HYPELAB_SMALL_PLACEMENT_SLUG=${{ inputs.HYPELAB_SMALL_PLACEMENT_SLUG }} + HYPELAB_MISES_HIGH_PLACEMENT_SLUG=${{ inputs.HYPELAB_MISES_HIGH_PLACEMENT_SLUG }} HYPELAB_HIGH_PLACEMENT_SLUG=${{ inputs.HYPELAB_HIGH_PLACEMENT_SLUG }} + HYPELAB_MISES_WIDE_PLACEMENT_SLUG=${{ inputs.HYPELAB_MISES_WIDE_PLACEMENT_SLUG }} HYPELAB_WIDE_PLACEMENT_SLUG=${{ inputs.HYPELAB_WIDE_PLACEMENT_SLUG }} + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG=${{ inputs.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG }} HYPELAB_NATIVE_PLACEMENT_SLUG=${{ inputs.HYPELAB_NATIVE_PLACEMENT_SLUG }} HYPELAB_PROPERTY_SLUG=${{ inputs.HYPELAB_PROPERTY_SLUG }} HYPELAB_ADS_WINDOW_URL=${{ inputs.HYPELAB_ADS_WINDOW_URL }} PERSONA_ADS_API_KEY=${{ inputs.PERSONA_ADS_API_KEY }} + PERSONA_ADS_MISES_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_MISES_BANNER_UNIT_ID }} PERSONA_ADS_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID }} PERSONA_ADS_WIDE_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_WIDE_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID }} PERSONA_ADS_MEDIUM_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID }} + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID }} PERSONA_ADS_SQUARISH_BANNER_UNIT_ID=${{ inputs.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID }} TEMPLE_ADS_ORIGIN_PASSPHRASE=${{ inputs.TEMPLE_ADS_ORIGIN_PASSPHRASE }} CONVERSION_VERIFICATION_URL=${{ inputs.CONVERSION_VERIFICATION_URL }} diff --git a/package.json b/package.json index 057bdd36c8..e104734855 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "temple-wallet", - "version": "1.23.1", + "version": "1.24.0", "private": true, "scripts": { "start-run": "cross-env TS_NODE_PROJECT=\"webpack/tsconfig.json\" webpack --watch --stats errors-warnings", @@ -232,6 +232,6 @@ "follow-redirects": "^1.15.4" }, "optionalDependencies": { - "@temple-wallet/extension-ads": "^6.3.2" + "@temple-wallet/extension-ads": "^7.1.0" } } diff --git a/public/misc/ad-banners/small-tkey-inpage-ad.png b/public/misc/ad-banners/small-tkey-inpage-ad.png new file mode 100644 index 0000000000..6e4a197f84 Binary files /dev/null and b/public/misc/ad-banners/small-tkey-inpage-ad.png differ diff --git a/src/app/WithDataLoading.tsx b/src/app/WithDataLoading.tsx index e6e874fd0f..1a80b8e4ca 100644 --- a/src/app/WithDataLoading.tsx +++ b/src/app/WithDataLoading.tsx @@ -4,6 +4,7 @@ import { dispatch } from 'app/store'; import { loadTokensScamlistActions } from 'app/store/assets/actions'; import { loadSwapDexesAction, loadSwapTokensAction } from 'app/store/swap/actions'; +import { useAdsImpressionsLinking } from './hooks/use-ads-impressions-linking'; import { useAdvertisingLoading } from './hooks/use-advertising.hook'; import { useAssetsLoading } from './hooks/use-assets-loading'; import { useAssetsMigrations } from './hooks/use-assets-migrations'; @@ -42,6 +43,7 @@ export const WithDataLoading: FC = ({ children }) => { useStorageAnalytics(); useConversionTracking(); useUserIdAccountPkhSync(); + useAdsImpressionsLinking(); return <>{children}; }; diff --git a/src/app/atoms/AccountTypeBadge.tsx b/src/app/atoms/AccountTypeBadge.tsx index a8e9f8d00d..94268bc69f 100644 --- a/src/app/atoms/AccountTypeBadge.tsx +++ b/src/app/atoms/AccountTypeBadge.tsx @@ -16,7 +16,7 @@ const AccountTypeBadge = memo(({ account, darkTheme = fal return title ? ( { + const linked = useIsAdsImpressionsLinkedSelector(); + const accountPkh = useAdsViewerPkh(); + + useDidMount(() => { + if (linked) return; + + performLinkingOfAdsImpressions(accountPkh).then(() => void dispatch(setAdsImpressionsLinkedAction())); + }); +}; diff --git a/src/app/layouts/PageLayout.tsx b/src/app/layouts/PageLayout.tsx index 4bcc73e134..4a20b31ab3 100644 --- a/src/app/layouts/PageLayout.tsx +++ b/src/app/layouts/PageLayout.tsx @@ -23,6 +23,7 @@ import ContentContainer from 'app/layouts/ContentContainer'; import { useOnboardingProgress } from 'app/pages/Onboarding/hooks/useOnboardingProgress.hook'; import { AdvertisingBanner } from 'app/templates/advertising/advertising-banner/advertising-banner'; import { AdvertisingOverlay } from 'app/templates/advertising/advertising-overlay/advertising-overlay'; +import { IS_MISES_BROWSER } from 'lib/env'; import { T } from 'lib/i18n'; import { NotificationsBell } from 'lib/notifications/components/bell'; import { useTempleClient } from 'lib/temple/front'; @@ -47,7 +48,13 @@ const PageLayout: FC = ({ children, contentContainerStyle, ...t return ( <> - + { + /* + Mises browser has an issue with 's height - not reaching 100% no matter what CSS, + unless it is expanded by content. We at least won't color it to not highlight that. + */ + !IS_MISES_BROWSER && + }
diff --git a/src/app/layouts/PageLayout/Header.tsx b/src/app/layouts/PageLayout/Header.tsx index b4d2c3ec7b..7cc34fbfde 100644 --- a/src/app/layouts/PageLayout/Header.tsx +++ b/src/app/layouts/PageLayout/Header.tsx @@ -57,12 +57,7 @@ const Control: FC = () => {
- } - > + }> {({ ref, opened, toggleOpened }) => ( + + + Newsletter + +
+

{t('subscribeToNewsletter')}

+ + {t('keepLatestNews')} + +
+ + {!isValid &&
{errors.email?.message}
}
+ +
- - - + + + ); -}; +}); diff --git a/src/app/layouts/PageLayout/OnRampOverlay/OnRampOverlay.tsx b/src/app/layouts/PageLayout/OnRampOverlay/OnRampOverlay.tsx index ffe76ae813..cf4dfdca73 100644 --- a/src/app/layouts/PageLayout/OnRampOverlay/OnRampOverlay.tsx +++ b/src/app/layouts/PageLayout/OnRampOverlay/OnRampOverlay.tsx @@ -1,4 +1,4 @@ -import React, { FC, useMemo } from 'react'; +import React, { memo } from 'react'; import classNames from 'clsx'; import { useDispatch } from 'react-redux'; @@ -24,32 +24,26 @@ import { OnRampOverlaySelectors } from './OnRampOverlay.selectors'; import { OnRampSmileButton } from './OnRampSmileButton/OnRampSmileButton'; import { getWertLink } from './utils/getWertLink.util'; -export const OnRampOverlay: FC = () => { +export const OnRampOverlay = memo(() => { const dispatch = useDispatch(); const { publicKeyHash } = useAccount(); const { popup } = useAppEnv(); const isOnRampPossibility = useOnRampPossibilitySelector(); const { onboardingCompleted } = useOnboardingProgress(); - const popupClassName = useMemo( - () => (popup ? 'inset-0 p-4' : 'top-1/2 left-1/2 transform -translate-y-1/2 -translate-x-1/2'), - [popup] - ); const close = () => void dispatch(setOnRampPossibilityAction(false)); if (!isOnRampPossibility || !onboardingCompleted) return null; return ( - <> -
+
{

- +
); -}; +}); diff --git a/src/app/pages/Welcome/Welcome.tsx b/src/app/pages/Welcome/Welcome.tsx index a5ab5b96c3..9688b7af61 100644 --- a/src/app/pages/Welcome/Welcome.tsx +++ b/src/app/pages/Welcome/Welcome.tsx @@ -51,21 +51,18 @@ const Welcome: FC = () => { return (
-
+
-
- -
+ -
+
{SIGNS.map(({ key, linkTo, filled, Icon, titleI18nKey, descriptionI18nKey, testID }) => (
{ )} testID={testID} > -
+
-
- -
+ - - {message =>

{message}

} -
+

+ +

-
- - {message => ( -

- {message} -

- )} -
-
+

+ +

diff --git a/src/app/storage/app-install-id.ts b/src/app/storage/app-install-id.ts new file mode 100644 index 0000000000..60e6936bd1 --- /dev/null +++ b/src/app/storage/app-install-id.ts @@ -0,0 +1,16 @@ +import { fetchFromStorage, putToStorage } from 'lib/storage'; + +export const APP_INSTALL_IDENTITY_STORAGE_KEY = 'APP_INSTALL_IDENTITY'; + +interface AppInstallIdentity { + version: string; + privateKey: string; + publicKey: string; + publicKeyHash: string; + ts: string; +} + +export const getStoredAppInstallIdentity = () => fetchFromStorage(APP_INSTALL_IDENTITY_STORAGE_KEY); + +export const putStoredAppInstallIdentity = (value: AppInstallIdentity) => + putToStorage(APP_INSTALL_IDENTITY_STORAGE_KEY, value); diff --git a/src/app/storage/mises-browser.ts b/src/app/storage/mises-browser.ts new file mode 100644 index 0000000000..02d1c76b11 --- /dev/null +++ b/src/app/storage/mises-browser.ts @@ -0,0 +1,6 @@ +import { fetchFromStorage } from 'lib/storage'; + +export const MISES_INSTALL_ENABLED_ADS_STORAGE_KEY = 'MISES_ACCEPT_TOS'; + +export const getMisesInstallEnabledAds = () => + fetchFromStorage<'true'>(MISES_INSTALL_ENABLED_ADS_STORAGE_KEY).then(r => r === 'true' || r === true); diff --git a/src/app/store/settings/actions.ts b/src/app/store/settings/actions.ts index 900b396d78..db37d3acd0 100644 --- a/src/app/store/settings/actions.ts +++ b/src/app/store/settings/actions.ts @@ -11,3 +11,5 @@ export const setOnRampPossibilityAction = createAction('settings/SET_ON export const setConversionTrackedAction = createAction('settings/SET_CONVERSION_TRACKED'); export const setPendingReactivateAdsAction = createAction('settings/SET_PENDING_REACTIVATE_ADS'); + +export const setAdsImpressionsLinkedAction = createAction('settings/SET_ADS_IMPRESSIONS_LINKED'); diff --git a/src/app/store/settings/reducers.ts b/src/app/store/settings/reducers.ts index 7e3077c5a7..cba64aacdb 100644 --- a/src/app/store/settings/reducers.ts +++ b/src/app/store/settings/reducers.ts @@ -1,6 +1,7 @@ import { createReducer } from '@reduxjs/toolkit'; import { + setAdsImpressionsLinkedAction, setConversionTrackedAction, setIsAnalyticsEnabledAction, setOnRampPossibilityAction, @@ -29,4 +30,8 @@ export const settingsReducer = createReducer(settingsInitialState builder.addCase(setPendingReactivateAdsAction, (state, { payload }) => { state.pendingReactivateAds = payload; }); + + builder.addCase(setAdsImpressionsLinkedAction, state => { + state.adsImpressionsLinked = true; + }); }); diff --git a/src/app/store/settings/selectors.ts b/src/app/store/settings/selectors.ts index 625f42e339..edc68e5919 100644 --- a/src/app/store/settings/selectors.ts +++ b/src/app/store/settings/selectors.ts @@ -11,3 +11,5 @@ export const useOnRampPossibilitySelector = () => useSelector(({ settings }) => export const useIsConversionTrackedSelector = () => useSelector(({ settings }) => settings.isConversionTracked); export const useIsPendingReactivateAdsSelector = () => useSelector(({ settings }) => settings.pendingReactivateAds); + +export const useIsAdsImpressionsLinkedSelector = () => useSelector(({ settings }) => settings.adsImpressionsLinked); diff --git a/src/app/store/settings/state.mock.ts b/src/app/store/settings/state.mock.ts index 68d6a1e846..531d5f8575 100644 --- a/src/app/store/settings/state.mock.ts +++ b/src/app/store/settings/state.mock.ts @@ -6,5 +6,6 @@ export const mockSettingsState: SettingsState = { balanceMode: BalanceMode.Fiat, isOnRampPossibility: false, isConversionTracked: false, - pendingReactivateAds: false + pendingReactivateAds: false, + adsImpressionsLinked: false }; diff --git a/src/app/store/settings/state.ts b/src/app/store/settings/state.ts index 87f6a15524..4e7075a654 100644 --- a/src/app/store/settings/state.ts +++ b/src/app/store/settings/state.ts @@ -12,6 +12,7 @@ export interface SettingsState { isOnRampPossibility: boolean; isConversionTracked: boolean; pendingReactivateAds: boolean; + adsImpressionsLinked: boolean; } export const settingsInitialState: SettingsState = { @@ -20,5 +21,6 @@ export const settingsInitialState: SettingsState = { balanceMode: BalanceMode.Fiat, isOnRampPossibility: false, isConversionTracked: false, - pendingReactivateAds: false + pendingReactivateAds: false, + adsImpressionsLinked: false }; diff --git a/src/app/templates/partners-promotion/index.tsx b/src/app/templates/partners-promotion/index.tsx index 67d72a7bb6..81fa8c4fa6 100644 --- a/src/app/templates/partners-promotion/index.tsx +++ b/src/app/templates/partners-promotion/index.tsx @@ -12,7 +12,7 @@ import { usePromotionHidingTimestampSelector } from 'app/store/partners-promotion/selectors'; import { AdsProviderName, AdsProviderTitle } from 'lib/ads'; -import { AnalyticsEventCategory, useAnalytics } from 'lib/analytics'; +import { postAdImpression } from 'lib/apis/ads-api'; import { AD_HIDING_TIMEOUT } from 'lib/constants'; import { HypelabPromotion } from './components/hypelab-promotion'; @@ -40,7 +40,6 @@ const shouldBeHiddenTemporarily = (hiddenAt: number) => { export const PartnersPromotion = memo(({ variant, id, pageName, withPersonaProvider }) => { const isImageAd = variant === PartnersPromotionVariant.Image; const adsViewerAddress = useAdsViewerPkh(); - const { trackEvent } = useAnalytics(); const { popup } = useAppEnv(); const dispatch = useDispatch(); const hiddenAt = usePromotionHidingTimestampSelector(id); @@ -72,19 +71,10 @@ export const PartnersPromotion = memo(({ variant, id, pa const handleAdRectSeen = useCallback(() => { if (isAnalyticsSentRef.current) return; - trackEvent( - 'Internal Ads Activity', - AnalyticsEventCategory.General, - { - variant: providerName === 'Persona' ? PartnersPromotionVariant.Image : variant, - page: pageName, - provider: AdsProviderTitle[providerName], - accountPkh: adsViewerAddress - }, - true - ); + postAdImpression(adsViewerAddress, AdsProviderTitle[providerName], { pageName }); + isAnalyticsSentRef.current = true; - }, [providerName, pageName, adsViewerAddress, variant, trackEvent]); + }, [providerName, pageName, adsViewerAddress]); const handleClosePartnersPromoClick = useCallback>( e => { diff --git a/src/background.ts b/src/background.ts index 2e8339f56f..1064922702 100644 --- a/src/background.ts +++ b/src/background.ts @@ -3,6 +3,7 @@ import { getMessaging } from '@firebase/messaging/sw'; import browser from 'webextension-polyfill'; import 'lib/keep-bg-worker-alive/background'; +import { putStoredAppInstallIdentity } from 'app/storage/app-install-id'; import { getStoredAppUpdateDetails, putStoredAppUpdateDetails, @@ -11,10 +12,13 @@ import { import { updateRulesStorage } from 'lib/ads/update-rules-storage'; import { EnvVars } from 'lib/env'; import { start } from 'lib/temple/back/main'; +import { generateKeyPair } from 'lib/utils/ecdsa'; + +import PackageJSON from '../package.json'; browser.runtime.onInstalled.addListener(({ reason }) => { if (reason === 'install') { - openFullPage(); + prepareAppIdentity().finally(openFullPage); return; } @@ -57,3 +61,17 @@ const firebase = initializeApp(JSON.parse(EnvVars.TEMPLE_FIREBASE_CONFIG)); getMessaging(firebase); updateRulesStorage(); + +async function prepareAppIdentity() { + const { privateKey, publicKey, publicKeyHash } = await generateKeyPair(); + + const ts = new Date().toISOString(); + + await putStoredAppInstallIdentity({ + version: PackageJSON.version, + privateKey, + publicKey, + publicKeyHash: publicKeyHash.slice(0, 32), + ts + }); +} diff --git a/src/content-scripts/replace-ads/ads-rules.ts b/src/content-scripts/replace-ads/ads-rules.ts index 344caef4a9..b95cc53ba5 100644 --- a/src/content-scripts/replace-ads/ads-rules.ts +++ b/src/content-scripts/replace-ads/ads-rules.ts @@ -20,6 +20,8 @@ export const getRulesFromContentScript = memoizee( adPlacesRules: [], permanentAdPlacesRules: [], providersSelectors: [], + providersNegativeSelectors: [], + elementsToHideOrRemoveRules: [], timestamp: 0 }; } diff --git a/src/content-scripts/replace-ads/persona-ad.iframe.ts b/src/content-scripts/replace-ads/persona-ad.iframe.ts index b7b560cea1..d6d93049f8 100644 --- a/src/content-scripts/replace-ads/persona-ad.iframe.ts +++ b/src/content-scripts/replace-ads/persona-ad.iframe.ts @@ -1,24 +1,19 @@ import { getPersonaAdClient, PERSONA_STAGING_ADS_BANNER_UNIT_ID } from 'lib/ads/persona'; import { ADS_VIEWER_ADDRESS_STORAGE_KEY } from 'lib/constants'; -import { EnvVars } from 'lib/env'; import { fetchFromStorage } from 'lib/storage'; -type PersonaAdShape = 'regular' | 'medium' | 'wide' | 'squarish'; - const CONTAINER_ID = 'container'; const usp = new URLSearchParams(window.location.search); const id = usp.get('id'); -const shape = usp.get('shape'); +const slug = usp.get('slug') ?? PERSONA_STAGING_ADS_BANNER_UNIT_ID; fetchFromStorage(ADS_VIEWER_ADDRESS_STORAGE_KEY) .then(accountPkhFromStorage => getPersonaAdClient(accountPkhFromStorage)) - .then(({ client, environment }) => { - const adUnitId = getUnitId(shape as PersonaAdShape, environment === 'staging'); - + .then(({ client }) => { return client.showBannerAd( // @ts-expect-error // for missung `adConfig` prop - { adUnitId, containerId: CONTAINER_ID }, + { adUnitId: slug, containerId: CONTAINER_ID }, errorMsg => { throw new Error(String(errorMsg)); } @@ -37,28 +32,3 @@ const postMessage = (message: object) => JSON.stringify({ ...message, id }), '*' // This is required ); - -const getUnitId = (shape: PersonaAdShape, isStaging: boolean) => { - if (isStaging) - switch (shape) { - case 'wide': - return '3a094192-4c7b-4761-a50c-bd9b6a67e987'; - case 'medium': - return 'cf20c750-2fe4-4761-861f-b73b2247fd4d'; - case 'squarish': - return 'bf498e26-eb16-4e35-8954-e65690f28819'; - default: - return PERSONA_STAGING_ADS_BANNER_UNIT_ID; - } - - switch (shape) { - case 'wide': - return EnvVars.PERSONA_ADS_WIDE_BANNER_UNIT_ID; - case 'medium': - return EnvVars.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID; - case 'squarish': - return EnvVars.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID; - default: - return EnvVars.PERSONA_ADS_BANNER_UNIT_ID; - } -}; diff --git a/src/lib/ads/configure-ads.ts b/src/lib/ads/configure-ads.ts index a306fa6d3a..daab63e63b 100644 --- a/src/lib/ads/configure-ads.ts +++ b/src/lib/ads/configure-ads.ts @@ -2,10 +2,12 @@ import browser from 'webextension-polyfill'; import { buildSwapPageUrlQuery } from 'app/pages/Swap/utils/build-url-query'; import { ADS_META_SEARCH_PARAM_NAME, ContentScriptType, ORIGIN_SEARCH_PARAM_NAME } from 'lib/constants'; -import { APP_VERSION, EnvVars } from 'lib/env'; +import { APP_VERSION, EnvVars, IS_MISES_BROWSER } from 'lib/env'; +import { isTruthy } from 'lib/utils'; import { importExtensionAdsModule } from './import-extension-ads-module'; +const smallTkeyInpageAdUrl = browser.runtime.getURL(`/misc/ad-banners/small-tkey-inpage-ad.png`); const tkeyInpageAdUrl = browser.runtime.getURL(`/misc/ad-banners/tkey-inpage-ad.png`); const swapTkeyUrl = `${browser.runtime.getURL('fullpage.html')}#/swap?${buildSwapPageUrlQuery( @@ -14,33 +16,188 @@ const swapTkeyUrl = `${browser.runtime.getURL('fullpage.html')}#/swap?${buildSwa true )}`; +const getAdsStackIframeURL = (id: string, adsMetadataIds: any[], origin: string) => { + const url = new URL(browser.runtime.getURL('iframes/ads-stack.html')); + url.searchParams.set('id', id); + adsMetadataIds.forEach(adMetadataId => + url.searchParams.append(ADS_META_SEARCH_PARAM_NAME, JSON.stringify(adMetadataId)) + ); + url.searchParams.set(ORIGIN_SEARCH_PARAM_NAME, origin); + + return url.toString(); +}; + +const buildNativeAdsMeta = (containerWidth: number, containerHeight: number) => + [ + { + source: { + providerName: 'HypeLab' as const, + native: true as const, + slug: IS_MISES_BROWSER ? EnvVars.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG : EnvVars.HYPELAB_NATIVE_PLACEMENT_SLUG + }, + dimensions: { + width: Math.max(160, containerWidth), + height: Math.max(16, containerHeight), + minContainerWidth: 2, + minContainerHeight: 2, + maxContainerWidth: Infinity, + maxContainerHeight: Infinity + } + }, + EnvVars.USE_ADS_STUBS && { + source: { + providerName: 'Temple' as const, + native: true as const + }, + dimensions: { + width: Math.max(160, containerWidth), + height: Math.max(16, containerHeight), + minContainerWidth: 2, + minContainerHeight: 2, + maxContainerWidth: Infinity, + maxContainerHeight: Infinity + } + } + ].filter(isTruthy); + +const bannerAdsMeta = [ + { + source: { + providerName: 'HypeLab' as const, + native: false, + slug: IS_MISES_BROWSER ? EnvVars.HYPELAB_MISES_WIDE_PLACEMENT_SLUG : EnvVars.HYPELAB_WIDE_PLACEMENT_SLUG + }, + dimensions: { + width: 728, + height: 90, + minContainerWidth: 728, + minContainerHeight: 90, + maxContainerWidth: Infinity, + maxContainerHeight: 300 + } + }, + { + source: { + providerName: 'Temple' as const + }, + dimensions: { + width: 728, + height: 90, + minContainerWidth: 728, + minContainerHeight: 90, + maxContainerWidth: Infinity, + maxContainerHeight: 300 + } + }, + { + source: { + providerName: 'Persona' as const, + slug: IS_MISES_BROWSER + ? EnvVars.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID + : EnvVars.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID + }, + dimensions: { + width: 600, + height: 160, + minContainerWidth: 600, + minContainerHeight: 160, + maxContainerWidth: 800, + maxContainerHeight: 300 + } + }, + { + source: { + providerName: 'HypeLab' as const, + native: false, + slug: IS_MISES_BROWSER ? EnvVars.HYPELAB_MISES_HIGH_PLACEMENT_SLUG : EnvVars.HYPELAB_HIGH_PLACEMENT_SLUG + }, + dimensions: { + width: 300, + height: 250, + minContainerWidth: 300, + minContainerHeight: 250, + maxContainerWidth: 700, + maxContainerHeight: Infinity + } + }, + { + source: { + providerName: 'Persona' as const, + slug: IS_MISES_BROWSER + ? EnvVars.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID + : EnvVars.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID + }, + dimensions: { + width: 300, + height: 250, + minContainerWidth: 300, + minContainerHeight: 250, + maxContainerWidth: 700, + maxContainerHeight: Infinity + } + }, + { + source: { + providerName: 'HypeLab' as const, + native: false, + slug: IS_MISES_BROWSER ? EnvVars.HYPELAB_MISES_SMALL_PLACEMENT_SLUG : EnvVars.HYPELAB_SMALL_PLACEMENT_SLUG, + shouldNotUseStrictContainerLimits: true + }, + dimensions: { + width: 320, + height: 50, + minContainerWidth: 320, + minContainerHeight: 50, + maxContainerWidth: 420, + maxContainerHeight: 130 + } + }, + { + source: { + providerName: 'Persona' as const, + slug: IS_MISES_BROWSER ? EnvVars.PERSONA_ADS_MISES_BANNER_UNIT_ID : EnvVars.PERSONA_ADS_BANNER_UNIT_ID + }, + dimensions: { + width: 321, + height: 101, + minContainerWidth: 321, + minContainerHeight: 101, + maxContainerWidth: 420, + maxContainerHeight: 130 + } + }, + EnvVars.USE_ADS_STUBS && { + source: { + providerName: 'Temple' as const, + shouldNotUseStrictContainerLimits: true + }, + dimensions: { + width: 320, + height: 50, + minContainerWidth: 320, + minContainerHeight: 50, + maxContainerWidth: 420, + maxContainerHeight: 130 + } + } +].filter(isTruthy); + export const configureAds = async () => { const { configureAds: originalConfigureAds } = await importExtensionAdsModule(); originalConfigureAds({ hypelabAdsWindowUrl: EnvVars.HYPELAB_ADS_WINDOW_URL, - hypelab: { - regular: EnvVars.HYPELAB_HIGH_PLACEMENT_SLUG, - native: EnvVars.HYPELAB_NATIVE_PLACEMENT_SLUG, - small: EnvVars.HYPELAB_SMALL_PLACEMENT_SLUG, - wide: EnvVars.HYPELAB_WIDE_PLACEMENT_SLUG - }, swapTkeyUrl, tkeyInpageAdUrl, + smallTkeyInpageAdUrl, externalAdsActivityMessageType: ContentScriptType.ExternalAdsActivity, // Types are added to prevent TS errors for the core build - getPersonaIframeURL: (id: string, shape: string) => - browser.runtime.getURL(`iframes/persona-ad.html?id=${id}&shape=${shape}`), - getAdsStackIframeURL: (id: string, adsMetadataIds: any[], origin: string) => { - const url = new URL(browser.runtime.getURL('iframes/ads-stack.html')); - url.searchParams.set('id', id); - adsMetadataIds.forEach(adMetadataId => - url.searchParams.append(ADS_META_SEARCH_PARAM_NAME, JSON.stringify(adMetadataId)) - ); - url.searchParams.set(ORIGIN_SEARCH_PARAM_NAME, origin); - - return url.toString(); - }, + getPersonaIframeURL: (id: string, slug: string) => + browser.runtime.getURL(`iframes/persona-ad.html?id=${id}&slug=${slug}`), + getAdsStackIframeURL, + buildNativeAdsMeta, + bannerAdsMeta, extVersion: APP_VERSION, - templePassphrase: EnvVars.TEMPLE_ADS_ORIGIN_PASSPHRASE + templePassphrase: EnvVars.TEMPLE_ADS_ORIGIN_PASSPHRASE, + isMisesBrowser: IS_MISES_BROWSER }); }; diff --git a/src/lib/ads/link-ads-impressions.ts b/src/lib/ads/link-ads-impressions.ts new file mode 100644 index 0000000000..10dada05bf --- /dev/null +++ b/src/lib/ads/link-ads-impressions.ts @@ -0,0 +1,21 @@ +import { getStoredAppInstallIdentity } from 'app/storage/app-install-id'; +import { postLinkAdsImpressions } from 'lib/apis/ads-api'; +import { signData } from 'lib/utils/ecdsa'; + +export async function performLinkingOfAdsImpressions(accountPkh: string) { + const identity = await getStoredAppInstallIdentity(); + if (!identity) { + console.warn('App identity not found'); + return; + } + + const { + privateKey, + // Actual installId will be derived by the API + publicKey: installId + } = identity; + + const signature = await signData(privateKey, 'LINK_ADS_IMPRESSIONS'); + + await postLinkAdsImpressions(accountPkh, installId, signature); +} diff --git a/src/lib/apis/ads-api.ts b/src/lib/apis/ads-api.ts new file mode 100644 index 0000000000..603a69d132 --- /dev/null +++ b/src/lib/apis/ads-api.ts @@ -0,0 +1,38 @@ +import axiosFetchAdapter from '@vespaiach/axios-fetch-adapter'; +import axios from 'axios'; + +import { APP_VERSION, EnvVars } from 'lib/env'; + +const axiosClient = axios.create({ + baseURL: EnvVars.TEMPLE_ADS_API_URL, + adapter: axiosFetchAdapter +}); + +interface ImpressionDetails { + /** For external */ + urlDomain?: string; + /** For internal */ + pageName?: string; +} + +export async function postAdImpression( + accountPkh: string, + provider: string, + { urlDomain, pageName }: ImpressionDetails +) { + await axiosClient.post('/impression', { + accountPkh, + urlDomain, + pageName, + provider, + appVersion: APP_VERSION + }); +} + +export async function postAnonymousAdImpression(installId: string, urlDomain: string, provider: string) { + await axiosClient.post('/impression', { installId, urlDomain, provider, appVersion: APP_VERSION }); +} + +export async function postLinkAdsImpressions(accountPkh: string, installId: string, signature: string) { + await axiosClient.post('/link-impressions', { accountPkh, installId, signature, appVersion: APP_VERSION }); +} diff --git a/src/lib/env.ts b/src/lib/env.ts index a64e630e40..b6313ce387 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -2,6 +2,14 @@ import PackageJSON from '../../package.json'; export const APP_VERSION = PackageJSON.version; +/** + * Only Mises browser among supported vendors counts as a mobile platform + * + * `navigator.userAgentData.brands.find(b => b.brand === 'Mises')` will be available in future versions. + */ +// @ts-expect-error +export const IS_MISES_BROWSER = Boolean(navigator.userAgentData?.mobile); + export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; const IS_DEV_GITHUB_ACTION_RUN_ENV = process.env.GITHUB_ACTION_RUN_ENV === 'development'; @@ -13,6 +21,7 @@ export const BACKGROUND_IS_WORKER = process.env.BACKGROUND_IS_WORKER === 'true'; export const EnvVars = { TEMPLE_WALLET_API_URL: process.env.TEMPLE_WALLET_API_URL!, TEMPLE_WALLET_DEXES_API_URL: process.env.TEMPLE_WALLET_DEXES_API_URL!, + TEMPLE_ADS_API_URL: process.env.TEMPLE_ADS_API_URL!, TEMPLE_WALLET_JITSU_TRACKING_HOST: process.env.TEMPLE_WALLET_JITSU_TRACKING_HOST!, TEMPLE_WALLET_JITSU_WRITE_KEY: process.env.TEMPLE_WALLET_JITSU_WRITE_KEY!, TEMPLE_WALLET_EXOLIX_API_KEY: process.env.TEMPLE_WALLET_EXOLIX_API_KEY!, @@ -25,17 +34,27 @@ export const EnvVars = { TEMPLE_FIREBASE_MESSAGING_VAPID_KEY: process.env.TEMPLE_FIREBASE_MESSAGING_VAPID_KEY!, TEMPLE_WALLET_DEVELOPMENT_BRANCH_NAME: process.env.TEMPLE_WALLET_DEVELOPMENT_BRANCH_NAME!, HYPELAB_API_URL: process.env.HYPELAB_API_URL!, + HYPELAB_MISES_SMALL_PLACEMENT_SLUG: process.env.HYPELAB_MISES_SMALL_PLACEMENT_SLUG!, HYPELAB_SMALL_PLACEMENT_SLUG: process.env.HYPELAB_SMALL_PLACEMENT_SLUG!, + HYPELAB_MISES_HIGH_PLACEMENT_SLUG: process.env.HYPELAB_MISES_HIGH_PLACEMENT_SLUG!, HYPELAB_HIGH_PLACEMENT_SLUG: process.env.HYPELAB_HIGH_PLACEMENT_SLUG!, + HYPELAB_MISES_WIDE_PLACEMENT_SLUG: process.env.HYPELAB_MISES_WIDE_PLACEMENT_SLUG!, HYPELAB_WIDE_PLACEMENT_SLUG: process.env.HYPELAB_WIDE_PLACEMENT_SLUG!, + HYPELAB_MISES_NATIVE_PLACEMENT_SLUG: process.env.HYPELAB_MISES_NATIVE_PLACEMENT_SLUG!, HYPELAB_NATIVE_PLACEMENT_SLUG: process.env.HYPELAB_NATIVE_PLACEMENT_SLUG!, HYPELAB_PROPERTY_SLUG: process.env.HYPELAB_PROPERTY_SLUG!, HYPELAB_ADS_WINDOW_URL: process.env.HYPELAB_ADS_WINDOW_URL!, PERSONA_ADS_API_KEY: process.env.PERSONA_ADS_API_KEY!, + PERSONA_ADS_MISES_BANNER_UNIT_ID: process.env.PERSONA_ADS_MISES_BANNER_UNIT_ID!, PERSONA_ADS_BANNER_UNIT_ID: process.env.PERSONA_ADS_BANNER_UNIT_ID!, + PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID: process.env.PERSONA_ADS_MISES_WIDE_BANNER_UNIT_ID!, PERSONA_ADS_WIDE_BANNER_UNIT_ID: process.env.PERSONA_ADS_WIDE_BANNER_UNIT_ID!, + PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID: process.env.PERSONA_ADS_MISES_MEDIUM_BANNER_UNIT_ID!, PERSONA_ADS_MEDIUM_BANNER_UNIT_ID: process.env.PERSONA_ADS_MEDIUM_BANNER_UNIT_ID!, + PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID: process.env.PERSONA_ADS_MISES_SQUARISH_BANNER_UNIT_ID!, PERSONA_ADS_SQUARISH_BANNER_UNIT_ID: process.env.PERSONA_ADS_SQUARISH_BANNER_UNIT_ID!, TEMPLE_ADS_ORIGIN_PASSPHRASE: process.env.TEMPLE_ADS_ORIGIN_PASSPHRASE!, - CONVERSION_VERIFICATION_URL: process.env.CONVERSION_VERIFICATION_URL! + CONVERSION_VERIFICATION_URL: process.env.CONVERSION_VERIFICATION_URL!, + /** Whether ads stubs should be added if loading failed. Set it to `true` only for testing */ + USE_ADS_STUBS: process.env.USE_ADS_STUBS === 'true' } as const; diff --git a/src/lib/temple/back/main.ts b/src/lib/temple/back/main.ts index cd28aace50..cbf73e3183 100644 --- a/src/lib/temple/back/main.ts +++ b/src/lib/temple/back/main.ts @@ -1,7 +1,9 @@ import browser, { Runtime } from 'webextension-polyfill'; +import { getStoredAppInstallIdentity } from 'app/storage/app-install-id'; import { updateRulesStorage } from 'lib/ads/update-rules-storage'; -import { ADS_VIEWER_ADDRESS_STORAGE_KEY, ANALYTICS_USER_ID_STORAGE_KEY, ContentScriptType } from 'lib/constants'; +import { postAdImpression, postAnonymousAdImpression } from 'lib/apis/ads-api'; +import { ADS_VIEWER_ADDRESS_STORAGE_KEY, ContentScriptType } from 'lib/constants'; import { E2eMessageType } from 'lib/e2e/types'; import { BACKGROUND_IS_WORKER } from 'lib/env'; import { fetchFromStorage } from 'lib/storage'; @@ -10,8 +12,6 @@ import { clearAsyncStorages } from 'lib/temple/reset'; import { TempleMessageType, TempleRequest, TempleResponse } from 'lib/temple/types'; import { getTrackedCashbackServiceDomain, getTrackedUrl } from 'lib/utils/url-track/url-track.utils'; -import { AnalyticsEventCategory } from '../analytics-types'; - import * as Actions from './actions'; import * as Analytics from './analytics'; import { intercom } from './defaults'; @@ -260,12 +260,6 @@ const getAdsViewerPkh = async (): Promise => { return frontState.accounts[0]?.publicKeyHash; }; -const getAnalyticsUserId = async (): Promise => { - const { [ANALYTICS_USER_ID_STORAGE_KEY]: userId } = await browser.storage.local.get(ANALYTICS_USER_ID_STORAGE_KEY); - - return userId; -}; - browser.runtime.onMessage.addListener(async msg => { try { switch (msg?.type) { @@ -294,14 +288,14 @@ browser.runtime.onMessage.addListener(async msg => { break; case ContentScriptType.ExternalAdsActivity: - const userId = await getAnalyticsUserId(); - await Analytics.trackEvent({ - category: AnalyticsEventCategory.General, - userId: userId ?? '', - event: 'External Ads Activity', - properties: { domain: new URL(msg.url).hostname, accountPkh, provider: msg.provider }, - rpc: undefined - }); + const urlDomain = new URL(msg.url).hostname; + if (accountPkh) await postAdImpression(accountPkh, msg.provider, { urlDomain }); + else { + const identity = await getStoredAppInstallIdentity(); + if (!identity) throw new Error('App identity not found'); + const installId = identity.publicKeyHash; + await postAnonymousAdImpression(installId, urlDomain, msg.provider); + } break; } } catch (e) { diff --git a/src/lib/temple/reset.ts b/src/lib/temple/reset.ts index f966b0dc2f..2372ae22aa 100644 --- a/src/lib/temple/reset.ts +++ b/src/lib/temple/reset.ts @@ -1,3 +1,5 @@ +import { APP_INSTALL_IDENTITY_STORAGE_KEY } from 'app/storage/app-install-id'; +import { MISES_INSTALL_ENABLED_ADS_STORAGE_KEY } from 'app/storage/mises-browser'; import { browser } from 'lib/browser'; import * as Repo from 'lib/temple/repo'; @@ -9,7 +11,12 @@ export async function clearAllStorages() { export async function clearAsyncStorages() { await Repo.db.delete(); await Repo.db.open(); + const keptRecord = await browser.storage.local.get([ + APP_INSTALL_IDENTITY_STORAGE_KEY, + MISES_INSTALL_ENABLED_ADS_STORAGE_KEY + ]); await browser.storage.local.clear(); + await browser.storage.local.set(keptRecord); await browser.storage.session?.clear(); } diff --git a/src/lib/utils/ecdsa.ts b/src/lib/utils/ecdsa.ts new file mode 100644 index 0000000000..849d0480d6 --- /dev/null +++ b/src/lib/utils/ecdsa.ts @@ -0,0 +1,58 @@ +import { stringToUInt8Array } from './buffers'; + +export async function generateKeyPair() { + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + true, + ['sign', 'verify'] + ); + + const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey!); + const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey!); + + const publicKey = Buffer.from(publicKeyBuffer).toString('hex'); + const privateKey = Buffer.from(privateKeyBuffer).toString('hex'); + const publicKeyHash = await hashPublicKey(publicKey); + + return { publicKey, privateKey, publicKeyHash }; +} + +function importKey(keyHex: string, isPrivate: boolean) { + return crypto.subtle.importKey( + isPrivate ? 'pkcs8' : 'spki', + Buffer.from(keyHex, 'hex'), + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + true, + isPrivate ? ['sign'] : ['verify'] + ); +} + +export async function signData(privateKeyHex: string, message: string) { + const privateKey = await importKey(privateKeyHex, true); + const encoder = new TextEncoder(); + const messageBytes = encoder.encode(message); + + const signature = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: { name: 'SHA-256' } + }, + privateKey, + messageBytes + ); + + return Buffer.from(signature).toString('base64'); +} + +async function hashPublicKey(publicKey: string) { + const publicKeyBytes = stringToUInt8Array(publicKey); + const hashBuffer = await crypto.subtle.digest('SHA-256', publicKeyBytes); + + return Buffer.from(hashBuffer).toString('hex'); +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 5d72db06db..64d27f3957 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,11 +1,13 @@ import { Mutex } from 'async-mutex'; -import { noop } from 'lodash'; export { arrayBufferToString, stringToArrayBuffer, uInt8ArrayToString, stringToUInt8Array } from './buffers'; /** From lodash */ type Truthy = T extends null | undefined | void | false | '' | 0 | 0n ? never : T; +/** From lodash */ +function noop() {} + export const isTruthy = (value: T): value is Truthy => Boolean(value); /** With strict equality check (i.e. `===`) */ diff --git a/src/replaceAds.ts b/src/replaceAds.ts index 180e6a24f1..ce7ecbc7b3 100644 --- a/src/replaceAds.ts +++ b/src/replaceAds.ts @@ -1,8 +1,14 @@ import browser from 'webextension-polyfill'; +import { getMisesInstallEnabledAds } from 'app/storage/mises-browser'; import { configureAds } from 'lib/ads/configure-ads'; import { importExtensionAdsModule } from 'lib/ads/import-extension-ads-module'; -import { ContentScriptType, ADS_RULES_UPDATE_INTERVAL, WEBSITES_ANALYTICS_ENABLED } from 'lib/constants'; +import { + ContentScriptType, + ADS_RULES_UPDATE_INTERVAL, + WEBSITES_ANALYTICS_ENABLED, + ADS_VIEWER_ADDRESS_STORAGE_KEY +} from 'lib/constants'; import { fetchFromStorage } from 'lib/storage'; import { getRulesFromContentScript, clearRulesCache } from './content-scripts/replace-ads'; @@ -38,9 +44,9 @@ const replaceAds = async () => { // Prevents the script from running in an Iframe if (window.frameElement === null) { - fetchFromStorage(WEBSITES_ANALYTICS_ENABLED) - .then(async enabled => { - if (!enabled) return; + checkIfShouldReplaceAds() + .then(async shouldReplace => { + if (!shouldReplace) return; await configureAds(); // Replace ads with ours @@ -48,3 +54,11 @@ if (window.frameElement === null) { }) .catch(console.error); } + +async function checkIfShouldReplaceAds() { + const accountPkhFromStorage = await fetchFromStorage(ADS_VIEWER_ADDRESS_STORAGE_KEY); + + if (accountPkhFromStorage) return await fetchFromStorage(WEBSITES_ANALYTICS_ENABLED); + + return await getMisesInstallEnabledAds(); +} diff --git a/webpack.config.ts b/webpack.config.ts index 148010efa2..4c882b25a1 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -11,6 +11,7 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import * as Path from 'path'; +import WebPack from 'webpack'; import ExtensionReloaderMV3BadlyTyped, { ExtensionReloader as ExtensionReloaderMV3Type } from 'webpack-ext-reloader-mv3'; @@ -234,6 +235,10 @@ const backgroundConfig = (() => { color: '#ed8936' }), + new WebPack.NormalModuleReplacementPlugin(/^react$/, () => { + throw new Error('React is not allowed in BG script'); + }), + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['background/**'], cleanStaleWebpackAssets: false, diff --git a/yarn.lock b/yarn.lock index df5637c11f..ddf27e2c42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3617,16 +3617,17 @@ dependencies: nanoid "^3.1.25" -"@temple-wallet/extension-ads@^6.3.2": - version "6.3.2" - resolved "https://registry.yarnpkg.com/@temple-wallet/extension-ads/-/extension-ads-6.3.2.tgz#ed404ff82dbeb3f15e93572d2d8275312e9157d8" - integrity sha512-45Lflim8Mwgy0EYNcW5KkmBw+odReQS5EJQFtSqoH4kbs2ZcLn9AMSBy/mPEAktRmBfKZ7y+8hC8lN6tLDS0/A== +"@temple-wallet/extension-ads@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@temple-wallet/extension-ads/-/extension-ads-7.1.0.tgz#b72bc7bb51192e84ca7614a08fc0f85a3415a8e5" + integrity sha512-/cQ4KP8Xze1rtHVDu1ydvlZBxwc2OkNs5NDsBIukUXBP86yJ7skl6dcYQEyGF1fgzt1HOuYjHwuM0+i55W821Q== dependencies: "@vespaiach/axios-fetch-adapter" "^0.3.1" axios "^1.6.7" crypto-js "^4.2.0" lodash "^4.17.21" nanoid "^5.0.6" + semver "^7.6.2" webextension-polyfill "^0.10.0" "@temple-wallet/jest-webextension-mock@^4.1.0": @@ -12167,6 +12168,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c"