From 8d61b8b9b013479ef00c37f115a45438d778115d Mon Sep 17 00:00:00 2001 From: koxuan Date: Thu, 17 Aug 2023 11:16:34 +0800 Subject: [PATCH] feat: toggle notifications with threshold settings --- packages/site/src/components/Buttons.tsx | 2 +- packages/site/src/components/Card.tsx | 5 +- packages/site/src/components/Header.tsx | 2 +- packages/site/src/components/Input.tsx | 6 +- packages/site/src/components/SnapLogo.tsx | 28 ++-- .../site/src/components/ToggleGeneric.tsx | 132 ++++++++++++++++++ packages/site/src/components/index.ts | 1 + packages/site/src/pages/index.tsx | 49 +++++-- packages/site/src/utils/snap.ts | 12 +- packages/site/src/utils/theme.ts | 11 ++ packages/snap/snap.manifest.json | 2 +- packages/snap/src/index.ts | 109 +++++++++++---- 12 files changed, 295 insertions(+), 64 deletions(-) create mode 100644 packages/site/src/components/ToggleGeneric.tsx diff --git a/packages/site/src/components/Buttons.tsx b/packages/site/src/components/Buttons.tsx index 3e8fe28..77e4190 100644 --- a/packages/site/src/components/Buttons.tsx +++ b/packages/site/src/components/Buttons.tsx @@ -95,7 +95,7 @@ export const ReconnectButton = (props: ComponentProps) => { }; export const SendHelloButton = (props: ComponentProps) => { - return ; + return ; }; export const HeaderButtons = ({ diff --git a/packages/site/src/components/Card.tsx b/packages/site/src/components/Card.tsx index 197c053..5d13e9c 100644 --- a/packages/site/src/components/Card.tsx +++ b/packages/site/src/components/Card.tsx @@ -7,6 +7,7 @@ type CardProps = { description: ReactNode; button?: ReactNode; input?: ReactNode; + toggle?: ReactNode; }; disabled?: boolean; fullWidth?: boolean; @@ -47,13 +48,15 @@ const Description = styled.div` `; export const Card = ({ content, disabled = false, fullWidth }: CardProps) => { - const { title, description, button, input } = content; + const { title, description, button, input , toggle} = content; return ( {title && ( {title} )} {description} + + {toggle} {input} {button} diff --git a/packages/site/src/components/Header.tsx b/packages/site/src/components/Header.tsx index 0aa4806..b07a02e 100644 --- a/packages/site/src/components/Header.tsx +++ b/packages/site/src/components/Header.tsx @@ -63,7 +63,7 @@ export const Header = ({ - template-snap + Notify Me ` padding: 0.5em; - margin: 0.5em; - color: ${props => props.$inputColor || 'gray'}; + margin-bottom: 2em; + color: ${props => props.$inputColor || 'black'}; border-radius: 3px; `; export const InputPlaceholder = (props) => { return (
- props.setThreshold({threshold: e.target.value})} type='text' /> + props.setThreshold({threshold: e.target.value})} type='text' />
); }; diff --git a/packages/site/src/components/SnapLogo.tsx b/packages/site/src/components/SnapLogo.tsx index c344f06..597cf88 100644 --- a/packages/site/src/components/SnapLogo.tsx +++ b/packages/site/src/components/SnapLogo.tsx @@ -1,14 +1,18 @@ export const SnapLogo = ({ color, size }: { color: string; size: number }) => ( - - - + // + // + // + + + + ); diff --git a/packages/site/src/components/ToggleGeneric.tsx b/packages/site/src/components/ToggleGeneric.tsx new file mode 100644 index 0000000..e4462bc --- /dev/null +++ b/packages/site/src/components/ToggleGeneric.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { getStopLossToggle } from '../utils'; + +type CheckedProps = { + readonly checked: boolean; +}; + +const ToggleWrapper = styled.div` + touch-action: pan-x; + display: inline-block; + position: relative; + cursor: pointer; + background-color: transparent; + border: 0; + padding: 0; + -webkit-touch-callout: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-tap-highlight-color: transparent; + margin-right: 2.4rem; + margin-bottom: 2.4rem; + ${({ theme }) => theme.mediaQueries.small} { + margin-right: 2.4rem; + } +`; + +const ToggleInput = styled.input` + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +`; + +const IconContainer = styled.div` + position: absolute; + width: 22px; + height: 22px; + top: 0; + bottom: 0; + margin-top: auto; + margin-bottom: auto; + line-height: 0; + opacity: 0; + transition: opacity 0.25s ease; + & > * { + align-items: center; + display: flex; + height: 22px; + justify-content: center; + position: relative; + width: 22px; + } +`; + +const CheckedContainer = styled(IconContainer)` + opacity: ${({ checked }) => (checked ? 1 : 0)}; + left: 10px; +`; + +const UncheckedContainer = styled(IconContainer)` + opacity: ${({ checked }) => (checked ? 0 : 1)}; + right: 10px; +`; + +const ToggleContainer = styled.div` + width: 68px; + height: 36px; + padding: 0; + border-radius: 36px; + background-color: ${({ theme }) => theme.colors.background.alternative}; + transition: all 0.2s ease; +`; +const ToggleCircle = styled.div` + transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; + position: absolute; + top: 4px; + left: ${({ checked }) => (checked ? '36px' : '4px')}; + width: 28px; + height: 28px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.14); + border-radius: 50%; + background-color: #ffffff; + box-sizing: border-box; + transition: all 0.25s ease; +`; + +export const ToggleGeneric = ({ + onToggle, + defaultChecked = false, +}: { + onToggle(): void; + defaultChecked?: boolean; +}) => { + useEffect(() => { + + + const f = async () => { + + const currentStop = await getStopLossToggle(); + setChecked(currentStop); + } + f(); + + }, []); + const [checked, setChecked] = useState(defaultChecked); + + const handleChange = () => { + onToggle(); + setChecked(!checked); + }; + + return ( + + + + 🔔 + + + + + + + + ); +}; diff --git a/packages/site/src/components/index.ts b/packages/site/src/components/index.ts index c91a0a2..693f3fa 100644 --- a/packages/site/src/components/index.ts +++ b/packages/site/src/components/index.ts @@ -6,3 +6,4 @@ export * from './MetaMask'; export * from './PoweredBy'; export * from './SnapLogo'; export * from './Toggle'; +export * from './ToggleGeneric'; diff --git a/packages/site/src/pages/index.tsx b/packages/site/src/pages/index.tsx index f3ae5df..d6a52eb 100644 --- a/packages/site/src/pages/index.tsx +++ b/packages/site/src/pages/index.tsx @@ -1,10 +1,12 @@ -import { useContext, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { MetamaskActions, MetaMaskContext } from '../hooks'; import { connectSnap, - getSnap, + getSnap, getStopLossToggle, + getThemePreference, sendHello, + sendToggleStop, shouldDisplayReconnectButton, } from '../utils'; import { @@ -13,6 +15,8 @@ import { ReconnectButton, SendHelloButton, Card, + Toggle, + ToggleGeneric, } from '../components'; import { InputPlaceholder } from '../components/Input'; @@ -23,6 +27,7 @@ const Container = styled.div` flex: 1; margin-top: 7.6rem; margin-bottom: 7.6rem; + ${({ theme }) => theme.mediaQueries.small} { padding-left: 2.4rem; padding-right: 2.4rem; @@ -47,6 +52,7 @@ const Subtitle = styled.p` font-weight: 500; margin-top: 0; margin-bottom: 0; + ${({ theme }) => theme.mediaQueries.small} { font-size: ${({ theme }) => theme.fontSizes.text}; } @@ -76,6 +82,7 @@ const Notice = styled.div` & > * { margin: 0; } + ${({ theme }) => theme.mediaQueries.small} { margin-top: 1.2rem; padding: 1.6rem; @@ -92,6 +99,7 @@ const ErrorMessage = styled.div` margin-top: 2.4rem; max-width: 60rem; width: 100%; + ${({ theme }) => theme.mediaQueries.small} { padding: 1.6rem; margin-bottom: 1.2rem; @@ -103,6 +111,7 @@ const ErrorMessage = styled.div` const Index = () => { const [state, dispatch] = useContext(MetaMaskContext); + const handleConnectClick = async () => { try { await connectSnap(); @@ -122,8 +131,8 @@ const Index = () => { threshold: '', }); const sendData = (data) => { - setData(data) - } + setData(data); + }; const handleSendHelloClick = async () => { try { @@ -134,15 +143,15 @@ const Index = () => { } }; - return ( - Welcome to template-snap + Notify Me - Get started by editing src/index.ts + Get started by clicking Install MetaMask Flask + Notify Me is a snap extension that allows crypto holders to be alerted when a token reaches a certain price. {state.error && ( @@ -154,7 +163,7 @@ const Index = () => { content={{ title: 'Install', description: - 'Snaps is pre-release software only available in MetaMask Flask, a canary distribution for developers with access to upcoming features.', + 'Notify Me allows crypto holders to be alerted when a token reaches a certain price.', button: , }} fullWidth @@ -194,16 +203,28 @@ const Index = () => { )} ), - input: , + input: , + toggle: ( + { + try { + console.log(await sendToggleStop(true)); + } catch (e) { + console.error(e); + dispatch({ type: MetamaskActions.SetError, payload: e }); + } + }} + /> + ), }} disabled={!state.installedSnap} fullWidth={ @@ -214,10 +235,8 @@ const Index = () => { />

- Please note that the snap.manifest.json and{' '} - package.json must be located in the server root directory and - the bundle must be hosted at the location specified by the location - field. + Please note that this is a proof of concept and is not meant to be{' '} + production ready

diff --git a/packages/site/src/utils/snap.ts b/packages/site/src/utils/snap.ts index 5a4f535..a1274cc 100644 --- a/packages/site/src/utils/snap.ts +++ b/packages/site/src/utils/snap.ts @@ -55,7 +55,6 @@ export const getSnap = async (version?: string): Promise => { */ export const sendHello = async (threshold: string) => { - console.log(threshold) await window.ethereum.request({ method: 'wallet_invokeSnap', params: { @@ -66,4 +65,15 @@ export const sendHello = async (threshold: string) => { } ; +export const sendToggleStop = async (toggleStop?: boolean) => { + return await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: defaultSnapOrigin, request: { method: 'toggle_stop', params: {to: toggleStop} }, + }, + }); + ; + } +; + export const isLocalSnap = (snapId: string) => snapId.startsWith('local:'); diff --git a/packages/site/src/utils/theme.ts b/packages/site/src/utils/theme.ts index a5b6a85..c584f2e 100644 --- a/packages/site/src/utils/theme.ts +++ b/packages/site/src/utils/theme.ts @@ -1,4 +1,5 @@ import { getLocalStorage, setLocalStorage } from './localStorage'; +import { sendToggleStop } from './snap'; /** * Get the user's preferred theme in local storage. @@ -25,3 +26,13 @@ export const getThemePreference = () => { return preference === 'dark'; }; + +export const getStopLossToggle = async () => { + const currentState = await sendToggleStop(); + console.log(currentState) + if("stop" in currentState && currentState["stop"] === true) { + return true; + } + console.log('asd') + return false; +}; diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 696a957..88db2b2 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/template-snap-monorepo.git" }, "source": { - "shasum": "ONyOhmKaywttmKNKBsr8XjgNVY80kqacswGPC0cWjNk=", + "shasum": "DZ9NdGSIJkRORpHrkKoLtUCx8Gp5JjtHSX89rlUuqQM=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 2a1a122..44a4b15 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -22,7 +22,6 @@ import type { OnCronjobHandler } from '@metamask/snaps-types'; import type { FetchParams } from './types'; - /** * Handle cronjob execution requests from MetaMask. This handler handles one * method: @@ -38,7 +37,6 @@ import type { FetchParams } from './types'; * @see https://docs.metamask.io/snaps/reference/exports/#oncronjob */ - /** * Fetch a JSON file from the provided URL. This uses the standard `fetch` * function to get the JSON data. Because of CORS, the server must respond with @@ -53,18 +51,78 @@ import type { FetchParams } from './types'; * @returns There response as JSON. * @throws If the provided URL is not a JSON document. */ + +const notify = (message: string) => { + snap.request({ + method: 'snap_notify', + params: { + type: 'native', + message: message, + }, + }); + + snap.request({ + method: 'snap_notify', + params: { + type: 'inApp', + message: message, + }, + }); +}; + async function getJson() { - const response = await fetch('https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=BTC,USD,EUR'); + const response = await fetch( + 'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=BTC,USD,EUR', + ); return await response.json(); } export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { + const currentState = await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, + }); switch (request.method) { case 'hello': { - await snap.request({ method: 'snap_manageState', - params: { operation: 'update', newState: { threshold: request.params.to } }, + params: { + operation: 'update', + newState: { ...currentState, threshold: request.params.to }, + }, + }); + + notify(`threshold set to $${request.params.to}`); + break; + } + + case 'toggle_stop': { + if (request.params.to) { + if ('stop' in currentState && currentState.stop) { + await snap.request({ + method: 'snap_manageState', + params: { + operation: 'update', + newState: { ...currentState, stop: false }, + }, + }); + notify('Stopped monitoring for stop loss ⛔'); + } else { + await snap.request({ + method: 'snap_manageState', + params: { + operation: 'update', + newState: { ...currentState, stop: true }, + }, + }); + + notify('Started monitoring for stop loss ✅'); + } + } + + return await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, }); break; } @@ -83,33 +141,26 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => { case 'execute': // At a later time, get the data stored. - const data = await snap.request({ + const data = await snap.request({ method: 'snap_manageState', params: { operation: 'get' }, }); - if("threshold" in data){ - return snap.request({ - method: 'snap_notify', - params: { - type: 'native', - message: `threshold is ${data.threshold}`, - }, - - }, - ); - } - else{ - return snap.request({ - method: 'snap_notify', - params: { - type: 'native', - message: `no threshold set`, - }, - - }, - ); - } - + // If the stop flag is not set or stop is false, return. + if (!('stop' in data) || ('stop' in data && !data.stop)) { + return; + } + + if ('threshold' in data && data.threshold) { + const price: number = (await getJson('')).USD; + if (parseFloat(price) < parseFloat(data.threshold)) { + const message: string = `Price $${price.toString()} below $${data.threshold.toString()}`; + + notify(message); + } + } else { + notify('threshold not set'); + } + break; default: throw new Error('Method not found.');