From c7bc0d7ee558da193d60e692564dffca17f0287f Mon Sep 17 00:00:00 2001 From: Lucas Werey <73439207+LucasWerey@users.noreply.github.com> Date: Tue, 8 Oct 2024 10:45:22 +0200 Subject: [PATCH] :technologist:(lld): add braze generator tools in dev settings (#8026) --- .changeset/little-comics-help.md | 5 + .../renderer/components/Carousel/helpers.tsx | 3 +- .../src/renderer/hooks/useActionCards.tsx | 10 +- .../src/renderer/modals/index.ts | 2 + .../src/renderer/modals/types.ts | 1 + .../BrazeTools/Hooks/useGenerateLocalBraze.ts | 141 +++++++++++ .../Developer/BrazeTools/Modal/Body.tsx | 239 ++++++++++++++++++ .../Developer/BrazeTools/Modal/index.tsx | 23 ++ .../sections/Developer/BrazeTools/index.tsx | 26 ++ .../settings/sections/Developer/index.tsx | 2 + .../src/types/dynamicContent.ts | 1 + .../static/i18n/en/app.json | 25 ++ 12 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 .changeset/little-comics-help.md create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Hooks/useGenerateLocalBraze.ts create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/Body.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/index.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/index.tsx diff --git a/.changeset/little-comics-help.md b/.changeset/little-comics-help.md new file mode 100644 index 000000000000..a129d7ff8423 --- /dev/null +++ b/.changeset/little-comics-help.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Add mocked generator braze CCs in dev settings diff --git a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx b/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx index 3543a018ccfe..456fdc240ea8 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Carousel/helpers.tsx @@ -92,14 +92,13 @@ export const useDefaultSlides = (): { : currentCard.id && dispatch(setDismissedContentCards({ id: currentCard.id, timestamp: Date.now() })); setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id)); - dispatch(setPortfolioCards(portfolioCards.filter(n => n.id !== slide.id))); } + dispatch(setPortfolioCards(portfolioCards.filter(n => n.id !== slide.id))); } } }, [portfolioCards, cachedContentCards, isTrackedUser, dispatch], ); - const logSlideClick = useCallback( (cardId: string) => { const currentCard = cachedContentCards.find(card => card.id === cardId); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx b/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx index f468e4af1bd4..3a2bb7f7347c 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx +++ b/apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx @@ -8,6 +8,7 @@ import { openURL } from "~/renderer/linking"; import { track } from "../analytics/segment"; import { trackingEnabledSelector } from "../reducers/settings"; import { setDismissedContentCards } from "../actions/settings"; +import { ActionContentCard } from "~/types/dynamicContent"; const useActionCards = () => { const dispatch = useDispatch(); @@ -37,9 +38,10 @@ const useActionCards = () => { : currentCard.id && dispatch(setDismissedContentCards({ id: currentCard.id, timestamp: Date.now() })); setCachedContentCards(cachedContentCards.filter(n => n.id !== currentCard.id)); - dispatch(setActionCards(actionCards.filter(n => n.id !== currentCard.id))); } - if (actionCard) { + dispatch(setActionCards(actionCards.filter((n: ActionContentCard) => n.id !== actionCard?.id))); + + if (actionCard && !actionCard.isMock) { track("contentcard_dismissed", { contentcard: actionCard.title, campaign: actionCard.id, @@ -53,6 +55,10 @@ const useActionCards = () => { const currentCard = findCard(cardId); const actionCard = findActionCard(cardId); + if (actionCard?.isMock) { + link && openURL(link); + } + if (currentCard) { // For some reason braze won't log the click event if the card url is empty // Setting it as the card id just to have a dummy non empty value diff --git a/apps/ledger-live-desktop/src/renderer/modals/index.ts b/apps/ledger-live-desktop/src/renderer/modals/index.ts index 5a8e78f49503..1f8eff5759c7 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/index.ts +++ b/apps/ledger-live-desktop/src/renderer/modals/index.ts @@ -34,6 +34,7 @@ import MODAL_HIDE_INSCRIPTION from "LLD/features/Collectibles/Ordinals/component import MODAL_WALLET_SYNC_DEBUGGER from "./WalletSyncDebugger"; import MODAL_SIMPLEHASH_TOOLS from "./SimpleHashTools"; +import MODAL_BRAZE_TOOLS from "../screens/settings/sections/Developer/BrazeTools/Modal"; type GlobalModals = MakeModalsType; @@ -65,6 +66,7 @@ const globalModals: GlobalModals = { MODAL_WALLET_SYNC_DEBUGGER, MODAL_SIMPLEHASH_TOOLS, MODAL_HIDE_INSCRIPTION, + MODAL_BRAZE_TOOLS, // Platform MODAL_PLATFORM_EXCHANGE_START, diff --git a/apps/ledger-live-desktop/src/renderer/modals/types.ts b/apps/ledger-live-desktop/src/renderer/modals/types.ts index 5b4a110dffca..d30baa4ab4e4 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/types.ts +++ b/apps/ledger-live-desktop/src/renderer/modals/types.ts @@ -56,6 +56,7 @@ export type GlobalModalData = { MODAL_LOTTIE_DEBUGGER: undefined; MODAL_WALLET_SYNC_DEBUGGER: undefined; MODAL_SIMPLEHASH_TOOLS: undefined; + MODAL_BRAZE_TOOLS: undefined; MODAL_CREATE_LOCAL_APP: { manifest?: LiveAppManifest; }; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Hooks/useGenerateLocalBraze.ts b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Hooks/useGenerateLocalBraze.ts new file mode 100644 index 000000000000..0f3e68bbdc2e --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Hooks/useGenerateLocalBraze.ts @@ -0,0 +1,141 @@ +import { useDispatch, useSelector } from "react-redux"; +import { + setPortfolioCards, + setActionCards, + setNotificationsCards, +} from "~/renderer/actions/dynamicContent"; +import { + portfolioContentCardSelector, + actionContentCardSelector, + notificationsContentCardSelector, +} from "~/renderer/reducers/dynamicContent"; +import { + PortfolioContentCard, + ActionContentCard, + NotificationContentCard, + LocationContentCard, +} from "~/types/dynamicContent"; + +const generateNewPortfolioCard = ( + title: string, + description: string, + image: string, + order?: number, + url?: string, +): PortfolioContentCard => ({ + id: String(Date.now()), + title, + description, + location: LocationContentCard.Portfolio, + image, + created: new Date(), + onClickOnSlide: () => {}, + order, + isMock: true, + url, +}); + +const generateNewActionCard = ( + title: string, + description: string, + image: string, + mainCta: string, + link: string, + secondaryCta: string, + order?: number, +): ActionContentCard => ({ + id: String(Date.now()), + title, + description, + location: LocationContentCard.Action, + image, + mainCta, + link, + secondaryCta, + created: new Date(), + order, + isMock: true, +}); + +const generateNewNotificationCard = ( + title: string, + description: string, + cta: string, + viewed: boolean, + url?: string, + path?: string, + order?: number, +): NotificationContentCard => ({ + id: String(Date.now()), + title, + description, + location: LocationContentCard.NotificationCenter, + cta, + viewed, + url, + path, + created: new Date(), + order, + isMock: true, +}); + +export const useGenerateLocalBraze = () => { + const dispatch = useDispatch(); + + const portfolioCards = useSelector(portfolioContentCardSelector); + const actionCards = useSelector(actionContentCardSelector); + const notificationCards = useSelector(notificationsContentCardSelector); + + const addLocalPortfolioCard = ( + title: string, + description: string, + image: string, + order?: number, + url?: string, + ) => { + const newCard = generateNewPortfolioCard(title, description, image, order, url); + dispatch(setPortfolioCards([...portfolioCards, newCard])); + }; + + const addLocalActionCard = ( + title: string, + description: string, + image: string, + mainCta: string, + link: string, + secondaryCta: string, + order?: number, + ) => { + const newCard = generateNewActionCard( + title, + description, + image, + mainCta, + link, + secondaryCta, + order, + ); + dispatch(setActionCards([...actionCards, newCard])); + }; + + const addLocalNotificationCard = ( + title: string, + description: string, + cta: string, + viewed: boolean, + url?: string, + path?: string, + order?: number, + ) => { + const newCard = generateNewNotificationCard(title, description, cta, viewed, url, path, order); + dispatch(setNotificationsCards([...notificationCards, newCard])); + }; + + const dismissLocalCards = () => { + dispatch(setPortfolioCards([])); + dispatch(setActionCards([])); + dispatch(setNotificationsCards([])); + }; + + return { addLocalPortfolioCard, addLocalActionCard, addLocalNotificationCard, dismissLocalCards }; +}; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/Body.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/Body.tsx new file mode 100644 index 000000000000..391d827ecac4 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/Body.tsx @@ -0,0 +1,239 @@ +import React, { useReducer } from "react"; +import styled from "styled-components"; +import { Button, Flex, Input, Text } from "@ledgerhq/react-ui"; +import { useGenerateLocalBraze } from "../Hooks/useGenerateLocalBraze"; +import { useTranslation } from "react-i18next"; + +type TabKey = "NotificationContentCard" | "ActionContentCard" | "PortfolioContentCard"; + +const FormRow = styled(Flex)` + align-items: center; + column-gap: 12px; +`; + +const Label = styled(Text)` + min-width: 120px; +`; + +const FullWidthInput = styled(Input)` + flex: 1; + min-width: 630px; +`; + +interface FormState { + title: string; + description: string; + image: string; + mainCta: string; + link: string; + secondaryCta: string; + cta: string; + url: string; + path: string; + order?: number; +} + +type FormAction = + | { type: "SET_FIELD"; field: keyof FormState; value: string | number | boolean } + | { type: "RESET_FORM" }; + +const initialState: FormState = { + title: "Dummy Title", + description: "Dummy Description", + image: + "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQec1piP0de4iTT4LlWAg_SSU8DRv12XEfqwQ&s", + mainCta: "Dummy Main CTA", + link: "https://www.ledger.com/", + secondaryCta: "Dummy Dismiss CTA", + cta: "Dummy CTA", + url: "https://www.ledger.com/", + path: "https://www.ledger.com/", + order: undefined, +}; + +const formReducer = (state: FormState, action: FormAction): FormState => { + switch (action.type) { + case "SET_FIELD": + return { ...state, [action.field]: action.value }; + case "RESET_FORM": + return initialState; + default: + return state; + } +}; + +export const ModalBody: React.FC = () => { + const { t } = useTranslation(); + const [formData, dispatch] = useReducer(formReducer, initialState); + + const { addLocalPortfolioCard, addLocalActionCard, addLocalNotificationCard, dismissLocalCards } = + useGenerateLocalBraze(); + + const handleAddCard = () => { + const { title, description, image, mainCta, link, secondaryCta, cta, url, path, order } = + formData; + if (selectedTab === "PortfolioContentCard") { + addLocalPortfolioCard(title, description, image, order, url); + } else if (selectedTab === "ActionContentCard") { + addLocalActionCard(title, description, image, mainCta, link, secondaryCta, order); + } else if (selectedTab === "NotificationContentCard") { + addLocalNotificationCard(title, description, cta, false, url, path, order); + } + dispatch({ type: "RESET_FORM" }); + }; + + const handleChange = (field: keyof FormState, value: string | number | boolean) => { + dispatch({ type: "SET_FIELD", field, value }); + }; + + const handleInputChange = + (field: keyof FormState) => (e: React.ChangeEvent) => { + handleChange(field, e.target.value); + }; + + const handleNumberChange = + (field: keyof FormState) => (e: React.ChangeEvent) => { + handleChange(field, Number(e.target.value)); + }; + + const tabs: { key: TabKey; label: string }[] = [ + { + key: "NotificationContentCard", + label: t("settings.developer.brazeTools.modal.fields.notification"), + }, + { + key: "ActionContentCard", + label: t("settings.developer.brazeTools.modal.fields.action"), + }, + { + key: "PortfolioContentCard", + label: t("settings.developer.brazeTools.modal.fields.portfolio"), + }, + ]; + + const [selectedTab, setSelectedTab] = useReducer( + (state: TabKey, action: TabKey) => action, + tabs[0].key, + ); + + const inputFields: Record< + TabKey, + { field: keyof FormState; placeholder: string; label: string }[] + > = { + PortfolioContentCard: [ + { + field: "image", + placeholder: "Image URL", + label: t("settings.developer.brazeTools.modal.fields.image"), + }, + { + field: "url", + placeholder: "URL", + label: t("settings.developer.brazeTools.modal.fields.url"), + }, + ], + ActionContentCard: [ + { + field: "image", + placeholder: "Image URL", + label: t("settings.developer.brazeTools.modal.fields.image"), + }, + { + field: "mainCta", + placeholder: "Main CTA", + label: t("settings.developer.brazeTools.modal.fields.mainCta"), + }, + { + field: "link", + placeholder: "Link", + label: t("settings.developer.brazeTools.modal.fields.link"), + }, + { + field: "secondaryCta", + placeholder: "Secondary CTA", + label: t("settings.developer.brazeTools.modal.fields.secondaryCta"), + }, + ], + NotificationContentCard: [ + { + field: "cta", + placeholder: "CTA", + label: t("settings.developer.brazeTools.modal.fields.cta"), + }, + { + field: "url", + placeholder: "URL", + label: t("settings.developer.brazeTools.modal.fields.url"), + }, + { + field: "path", + placeholder: "Path", + label: t("settings.developer.brazeTools.modal.fields.path"), + }, + ], + }; + + return ( + + + {tabs.map(tab => ( + + ))} + + + + + + + + + + + + + + + {inputFields[selectedTab].map(({ field, placeholder, label }) => ( + + + + + ))} + + + + + + + ); +}; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/index.tsx new file mode 100644 index 000000000000..e1496b8509b4 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/Modal/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Trans } from "react-i18next"; +import Modal, { ModalBody } from "~/renderer/components/Modal"; +import { ModalBody as BrazeModalBody } from "./Body"; + +const BrazeToolsGenerator = () => ( + ( + } + noScroll + render={() => } + /> + )} + /> +); + +export default BrazeToolsGenerator; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/index.tsx new file mode 100644 index 000000000000..cae8e121d6d8 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/BrazeTools/index.tsx @@ -0,0 +1,26 @@ +import React, { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { SettingsSectionRow } from "~/renderer/screens/settings/SettingsSection"; +import Button from "~/renderer/components/Button"; +import { openModal } from "~/renderer/actions/modals"; + +const BrazeTools = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const onOpenModal = useCallback( + () => dispatch(openModal("MODAL_BRAZE_TOOLS", undefined)), + [dispatch], + ); + return ( + + + + ); +}; +export default BrazeTools; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/index.tsx index 8ce2a622572a..cc7510b340d9 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Developer/index.tsx @@ -25,6 +25,7 @@ import WalletSyncTester from "./WalletSync/WalletSyncTester"; import SimpleHashTools from "./SimpleHashTools/SimpleHashTools"; import MockAppUpdate from "./MockAppUpdate"; import EnableAnalyticsConsole from "./EnableAnalyticsConsole"; +import BrazeTools from "./BrazeTools"; const Default = () => { const { t } = useTranslation(); @@ -107,6 +108,7 @@ const Default = () => { + {__DEV__ && (