diff --git a/.github/workflows/ecommerce-store-website.yml b/.github/workflows/ecommerce-store-website.yml index d3a39ea17..886bcca85 100644 --- a/.github/workflows/ecommerce-store-website.yml +++ b/.github/workflows/ecommerce-store-website.yml @@ -29,6 +29,8 @@ jobs: cd ecommerce-store-website npm ci export BASE_PATH=/demo + export ANALYTICS_URL=https://analytics-api-stage.smarttokenlabs.com + export ANALYTICS_JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJicmFuZCI6IkJyYW5kIENvbm5lY3RvciIsImNhbXBhaWduIjoiRGVtbyIsImNsaWVudF9pZCI6IlNUTCIsImlhdCI6MTY4MzE3MDk5Nn0.HhW_sUtU0LKLpK2_puK7pj63CkaXmFa5sJ_wfx1ASR8 npm run build short_sha="${GITHUB_SHA:0:7}" build_version="${GITHUB_REF_NAME}-${short_sha}" diff --git a/ecommerce-store-website/.env.example b/ecommerce-store-website/.env.example index b8a474832..49ce7c4ee 100644 --- a/ecommerce-store-website/.env.example +++ b/ecommerce-store-website/.env.example @@ -5,3 +5,6 @@ BASE_PATH=/ MC_API_URL= MC_API_KEY= MC_LIST_ID= + +ANALYTICS_URL= +ANALYTICS_JWT= diff --git a/ecommerce-store-website/next.config.js b/ecommerce-store-website/next.config.js index e8a0abaa7..6b25d1a6b 100644 --- a/ecommerce-store-website/next.config.js +++ b/ecommerce-store-website/next.config.js @@ -25,6 +25,8 @@ module.exports = phase => { APP_VERSION: PKG.version, APP_HOST: process.env.APP_HOST ?? process.env.VERCEL_URL, BASE_PATH: process.env.BASE_PATH, + ANALYTICS_URL: process.env.ANALYTICS_URL, + ANALYTICS_JWT: process.env.ANALYTICS_JWT, }; const basePath = process.env.BASE_PATH ?? ""; diff --git a/ecommerce-store-website/package-lock.json b/ecommerce-store-website/package-lock.json index e21794299..07a02bb37 100644 --- a/ecommerce-store-website/package-lock.json +++ b/ecommerce-store-website/package-lock.json @@ -7,6 +7,7 @@ "name": "stl-token-negotiator-web", "dependencies": { "@react-spring/web": "^9.3.1", + "@tokenscript/analytics-client": "^0.4.0", "@tokenscript/token-negotiator": "2.5.0", "@use-gesture/react": "^10.1.6", "body-scroll-lock": "^4.0.0-beta.0", @@ -2100,6 +2101,28 @@ "node": ">=14.16" } }, + "node_modules/@tokenscript/analytics-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tokenscript/analytics-client/-/analytics-client-0.4.0.tgz", + "integrity": "sha512-PLqXrVz4FrzrBvWFoLwAN/KvSca2wd9vhTTkJgOoryJR+Jex3flpKd21oFCzAf1rsPOitUFuP0kbITQipOJzBg==", + "dependencies": { + "@trpc/client": "^10.12.0", + "@trpc/server": "^10.12.0", + "uuid": "^9.0.0", + "zod": "^3.20.6" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/@tokenscript/analytics-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@tokenscript/attestation": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@tokenscript/attestation/-/attestation-0.4.3.tgz", @@ -2261,6 +2284,19 @@ "@babel/runtime": "7.x" } }, + "node_modules/@trpc/client": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.24.0.tgz", + "integrity": "sha512-5HQdfJNslhVcb3kbRnPtdD4f5iIKvqdwNn0wLAGNf6LyKAO9wxAyZHRgojZu0dpKkMeN/saTIy+sP/YHoOPdaA==", + "peerDependencies": { + "@trpc/server": "10.24.0" + } + }, + "node_modules/@trpc/server": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.24.0.tgz", + "integrity": "sha512-gR91SkPDcDqJ+05r3qhV3P99FlcWkJvzQR4dCljQtpEUM1xZto2mF7/1Vuv+ipZyuQ0N2wbJt8Mcy+osspFwNg==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -11704,6 +11740,14 @@ "node": ">=10" } }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zustand": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", @@ -13179,6 +13223,24 @@ "defer-to-connect": "^2.0.1" } }, + "@tokenscript/analytics-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tokenscript/analytics-client/-/analytics-client-0.4.0.tgz", + "integrity": "sha512-PLqXrVz4FrzrBvWFoLwAN/KvSca2wd9vhTTkJgOoryJR+Jex3flpKd21oFCzAf1rsPOitUFuP0kbITQipOJzBg==", + "requires": { + "@trpc/client": "^10.12.0", + "@trpc/server": "^10.12.0", + "uuid": "^9.0.0", + "zod": "^3.20.6" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, "@tokenscript/attestation": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@tokenscript/attestation/-/attestation-0.4.3.tgz", @@ -13303,6 +13365,17 @@ "pump": "^3.0.0" } }, + "@trpc/client": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.24.0.tgz", + "integrity": "sha512-5HQdfJNslhVcb3kbRnPtdD4f5iIKvqdwNn0wLAGNf6LyKAO9wxAyZHRgojZu0dpKkMeN/saTIy+sP/YHoOPdaA==", + "requires": {} + }, + "@trpc/server": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.24.0.tgz", + "integrity": "sha512-gR91SkPDcDqJ+05r3qhV3P99FlcWkJvzQR4dCljQtpEUM1xZto2mF7/1Vuv+ipZyuQ0N2wbJt8Mcy+osspFwNg==" + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -20864,6 +20937,11 @@ "toposort": "^2.0.2" } }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" + }, "zustand": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", diff --git a/ecommerce-store-website/package.json b/ecommerce-store-website/package.json index feee090dd..bf196875c 100644 --- a/ecommerce-store-website/package.json +++ b/ecommerce-store-website/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@react-spring/web": "^9.3.1", + "@tokenscript/analytics-client": "^0.4.0", "@tokenscript/token-negotiator": "2.5.0", "@use-gesture/react": "^10.1.6", "body-scroll-lock": "^4.0.0-beta.0", diff --git a/ecommerce-store-website/src/base/utils/stats.js b/ecommerce-store-website/src/base/utils/stats.js new file mode 100644 index 000000000..002fcb353 --- /dev/null +++ b/ecommerce-store-website/src/base/utils/stats.js @@ -0,0 +1,67 @@ +import Analytics from '@tokenscript/analytics-client'; + +const analyticsUrl = process.env.ANALYTICS_URL; +const jwtToken = process.env.ANALYTICS_JWT; + +const analyticsClient = new Analytics(analyticsUrl, jwtToken).client; + +// TODO: all the following event reporting functions can be replaced +// with `Analytics.connectTokenNegotiator` once TN supports multiple event hooks +export function sendWalletConnectedEvent({ + providerType, + blockchain, + chainId, + address, +}) { + analyticsClient.event({ + name: 'wallet-connected', + eventProperties: { + provider_type: providerType, + blockchain, + chain_id: chainId, + address, + }, + }); +} + +export function sendTokensSelectedEvent({ selectedTokens }) { + const eventProperties = Object.fromEntries( + Object.entries(selectedTokens).map(([collectionId, { tokens }]) => [ + collectionId, + tokens.map((token) => token.ticketIdNumber ?? token.tokenId), + ]) + ); + + analyticsClient.event({ + name: 'token-selected', + eventProperties, + }); +} + +export function sendTokenProofEvent({ + data: { address, messageToSign, signature }, + error, +}) { + const eventProperties = error + ? { error } + : { + address, + messageToSign, + signature, + }; + + analyticsClient.event({ + name: 'token-proof', + eventProperties, + }); +} + +const AGREE_TO_STATS_KEY = 'bc-agree-stats'; + +export function loadAgreeToStats() { + return localStorage.getItem(AGREE_TO_STATS_KEY) === 'true'; +} + +export function storeAgreeToStats(value) { + localStorage.setItem(AGREE_TO_STATS_KEY, value); +} diff --git a/ecommerce-store-website/src/providers/TokenContextProvider.js b/ecommerce-store-website/src/providers/TokenContextProvider.js index 30a33a68a..dbb4efa92 100644 --- a/ecommerce-store-website/src/providers/TokenContextProvider.js +++ b/ecommerce-store-website/src/providers/TokenContextProvider.js @@ -1,5 +1,6 @@ -import React, {createContext, useState, useEffect, useMemo} from "react"; +import React, {createContext, useState, useEffect, useMemo, useRef} from "react"; import { chainMap } from "src/base/utils/network"; +import {sendTokensSelectedEvent, sendTokenProofEvent, sendWalletConnectedEvent, loadAgreeToStats, storeAgreeToStats} from "src/base/utils/stats"; const TokenContext = createContext({ tokens: {}, @@ -66,6 +67,12 @@ const TokenContextProvider = (props) => { const [wallet, setWallet] = useState(); const [walletStatus, setWalletStatus] = useState(''); const [ chainId, setChainId ] = useState(''); + const [ agreeToStats, setAgreeToStats ] = useState(false); + const agreeToStatsValue = useRef(); + agreeToStatsValue.current = agreeToStats; + + useEffect(() => { setAgreeToStats(loadAgreeToStats()) }, []); + useEffect(() => { storeAgreeToStats(agreeToStats) }, [agreeToStats]); useEffect(() => { @@ -94,15 +101,18 @@ const TokenContextProvider = (props) => { window.negotiator = newNegotiator; newNegotiator.on("tokens-selected", (tokens) => { + if (agreeToStatsValue.current) sendTokensSelectedEvent(tokens); setTokens({...tokens.selectedTokens}); }); newNegotiator.on("token-proof", (result) => { + if (agreeToStatsValue.current) sendTokenProofEvent(result); setProof(result.data); }); newNegotiator.on("connected-wallet", (connectedWallet) => { if (connectedWallet) { + if (agreeToStatsValue.current) sendWalletConnectedEvent(connectedWallet) setWallet(connectedWallet); resetIssuers(connectedWallet.chainId); setWalletStatus(undefined); @@ -159,8 +169,8 @@ const TokenContextProvider = (props) => { } const tokenContextProviderValue = useMemo( - () => ({ tokens, negotiator, wallet, proof, walletStatus, chainId, switchChain }), - [tokens, negotiator, wallet, proof, walletStatus, chainId, switchChain] + () => ({ tokens, negotiator, wallet, proof, walletStatus, chainId, agreeToStats, setAgreeToStats, switchChain }), + [tokens, negotiator, wallet, proof, walletStatus, chainId, agreeToStats, switchChain] ); return ( diff --git a/ecommerce-store-website/src/ui/app/layout/layout.js b/ecommerce-store-website/src/ui/app/layout/layout.js index 10494d33a..70786ffea 100644 --- a/ecommerce-store-website/src/ui/app/layout/layout.js +++ b/ecommerce-store-website/src/ui/app/layout/layout.js @@ -7,6 +7,7 @@ import clsx from 'clsx'; // App import { Footer, Header } from 'ui/app'; import { LabsBanner } from 'ui/sections'; +import { StatsDisclaimer } from 'ui/sections'; // @@ -16,6 +17,7 @@ import { LabsBanner } from 'ui/sections'; export default function Layout({ className, children }) { return (
+
{ children } diff --git a/ecommerce-store-website/src/ui/sections/index.js b/ecommerce-store-website/src/ui/sections/index.js index 281d4b37d..0e7e94453 100644 --- a/ecommerce-store-website/src/ui/sections/index.js +++ b/ecommerce-store-website/src/ui/sections/index.js @@ -5,3 +5,4 @@ export { default as DemoHeader } from './demo-header'; export { default as DemoHero } from './demo-hero'; export { default as Hero } from './hero'; export { default as LabsBanner } from './labs-banner'; +export { default as StatsDisclaimer } from './stats-disclaimer'; diff --git a/ecommerce-store-website/src/ui/sections/stats-disclaimer/index.js b/ecommerce-store-website/src/ui/sections/stats-disclaimer/index.js new file mode 100644 index 000000000..9b4fbf417 --- /dev/null +++ b/ecommerce-store-website/src/ui/sections/stats-disclaimer/index.js @@ -0,0 +1,8 @@ + + +// +// Brand Connector Demo / UI / Sections / Stats Disclaimer +// + + +export { default } from './stats-disclaimer'; diff --git a/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.js b/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.js new file mode 100644 index 000000000..8ceffc657 --- /dev/null +++ b/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.js @@ -0,0 +1,39 @@ + + +// Dependencies +import React, { useContext } from 'react'; +import clsx from 'clsx'; + +// App +import { TokenContext } from 'src/providers/TokenContextProvider'; +import { Button } from 'ui/components'; + +// Styles +import styles from "./stats-disclaimer.module.scss"; + + +// +// Brand Connector Demo / UI / Sections / Stats Disclaimer +// + + +export default function StatsDisclaimer() { + const { agreeToStats, setAgreeToStats } = useContext(TokenContext); + + const handleOnClick = () => { + setAgreeToStats(true); + }; + + return !agreeToStats && ( +
+ + This page collect data for analytics purposes. + + +
+ ); +} diff --git a/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.module.scss b/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.module.scss new file mode 100644 index 000000000..ce2eb960e --- /dev/null +++ b/ecommerce-store-website/src/ui/sections/stats-disclaimer/stats-disclaimer.module.scss @@ -0,0 +1,52 @@ +@use 'styles/modules' as *; + + +// +// Brand Connector Demo / UI / Sections / Stats Disclamer +// + + +.s-stats-disclaimer { + $s: &; + + position: fixed; + right: 0; top: 0; left: 0; + z-index: 20; + width: 100%; + margin: 0 auto; padding: 0 calc( var(--grid-gutter-width) / 2 ); box-sizing: border-box; + display: flex; flex-flow: row nowrap; justify-content: center; + background-color: color-get( base-100 ); + + &_desc { + opacity: 0.4; + font-size: 0.8rem; + line-height: 34px + } + + &_button { + margin-left: 1rem; + margin-top: auto; + margin-bottom: auto; + height: 24px; + font-size: 0.8rem; + padding: 0px; + } +} + +/** + * Media Queries + * -------------------------------------------------- + */ + +/** Breakpoint: Small ----------------------- */ + +@include breakpoint-for( small ) using ( $bp ) { + + .s-stats-disclaimer { + $s: &; + + &_button { + width: auto; + } + } +}