diff --git a/package.json b/package.json index 5512379c..f24d2909 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "postcss": "^8.4.32", "postcss-loader": "^7.3.3", "prettier": "^3.0.3", + "react-router-dom": "^6.21.1", "tailwindcss": "^3.4.0", "ts-jest": "^29.1.1", "ts-loader": "^9.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6e04cd0..6a2f95b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,6 +220,9 @@ devDependencies: prettier: specifier: ^3.0.3 version: 3.1.0 + react-router-dom: + specifier: ^6.21.1 + version: 6.21.1(react-dom@18.2.0)(react@18.2.0) tailwindcss: specifier: ^3.4.0 version: 3.4.0(ts-node@10.9.2) @@ -1177,6 +1180,11 @@ packages: /@polka/url@1.0.0-next.24: resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + /@remix-run/router@1.14.1: + resolution: {integrity: sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==} + engines: {node: '>=14.0.0'} + dev: true + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -6324,6 +6332,29 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /react-router-dom@6.21.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-QCNrtjtDPwHDO+AO21MJd7yIcr41UetYt5jzaB9Y1UYaPTCnVuJq6S748g1dE11OQlCFIQg+RtAA1SEZIyiBeA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.14.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.21.1(react@18.2.0) + dev: true + + /react-router@6.21.1(react@18.2.0): + resolution: {integrity: sha512-W0l13YlMTm1YrpVIOpjCADJqEUpz1vm+CMo47RuFX4Ftegwm6KOYsL5G3eiE52jnJpKvzm6uB/vTKTPKM8dmkA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.14.1 + react: 18.2.0 + dev: true + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/components/Popup/PopupFooter/PopupFooter.scss b/src/components/Popup/PopupFooter/PopupFooter.scss deleted file mode 100644 index cfd8c176..00000000 --- a/src/components/Popup/PopupFooter/PopupFooter.scss +++ /dev/null @@ -1,11 +0,0 @@ -.footer { - display: flex; - align-items: center; - justify-content: center; - padding: 0 20px; - border-top: 1px solid #e2e8f0; - color: #4a5568; - flex-basis: 48px; - flex-shrink: 0; - font-size: 14px; -} diff --git a/src/components/Popup/PopupFooter/PopupFooter.tsx b/src/components/Popup/PopupFooter/PopupFooter.tsx deleted file mode 100644 index da5e2620..00000000 --- a/src/components/Popup/PopupFooter/PopupFooter.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import './PopupFooter.scss' - -import React from 'react' - -interface IProps { - isMonetizationReady: boolean -} - -const PopupFooter: React.FC = ({ isMonetizationReady }) => ( - -) - -export default PopupFooter diff --git a/src/components/Popup/PopupFooter/index.ts b/src/components/Popup/PopupFooter/index.ts deleted file mode 100644 index 90aeac0c..00000000 --- a/src/components/Popup/PopupFooter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './PopupFooter' diff --git a/src/components/Popup/PopupHeader/PopupHeader.scss b/src/components/Popup/PopupHeader/PopupHeader.scss deleted file mode 100644 index ef42dc82..00000000 --- a/src/components/Popup/PopupHeader/PopupHeader.scss +++ /dev/null @@ -1,26 +0,0 @@ -.header { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - height: 40px; - padding: 0 16px; - flex-basis: 48px; - flex-shrink: 0; - font-weight: 500; - - .logo { - height: 24px; - } - - .close-btn { - padding: 0; - border: 0; - background: transparent; - cursor: pointer; - - img { - height: 32px; - } - } -} diff --git a/src/components/Popup/PopupHeader/PopupHeader.tsx b/src/components/Popup/PopupHeader/PopupHeader.tsx deleted file mode 100644 index 52b3ac45..00000000 --- a/src/components/Popup/PopupHeader/PopupHeader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { runtime } from 'webextension-polyfill' - -const Logo = runtime.getURL('assets/images/logo.svg') -const Close = runtime.getURL('assets/images/close.svg') -import './PopupHeader.scss' - -const PopupHeader: React.FC = () => { - return ( -
- Web Monetization Logo -
Web Monetization
- -
- ) -} - -export default PopupHeader diff --git a/src/components/Popup/PopupHeader/index.ts b/src/components/Popup/PopupHeader/index.ts deleted file mode 100644 index 9a403d0e..00000000 --- a/src/components/Popup/PopupHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './PopupHeader' diff --git a/src/components/icons.tsx b/src/components/icons.tsx index ee34cb21..11950a58 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -18,6 +18,50 @@ export const Spinner = (props: React.SVGProps) => { ) } +export const ArrowBack = (props: React.SVGProps) => { + return ( + + + + + + + + + ) +} + +export const Settings = (props: React.SVGProps) => { + return ( + + + + + + + + + ) +} + export const DollarSign = (props: React.SVGProps) => { return ( { + const location = useLocation() + + const component = useMemo( + () => + location.pathname === `/${ROUTES.SETTINGS}` ? ( + + + + ) : ( + + + + ), + + [location], + ) + + return component +} + +export const Header = () => { + return ( +
+
+ Web Monetization Logo +

Web Monetization

+
+
+ +
+
+ ) +} diff --git a/src/components/layout/main-layout.tsx b/src/components/layout/main-layout.tsx new file mode 100644 index 00000000..92fabac9 --- /dev/null +++ b/src/components/layout/main-layout.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Outlet } from 'react-router-dom' + +import { Header } from './header' + +const Divider = () => { + return
+} + +export const MainLayout = () => { + return ( +
+
+ +
+ +
+
+ ) +} diff --git a/src/components/router-provider.tsx b/src/components/router-provider.tsx new file mode 100644 index 00000000..076bcd74 --- /dev/null +++ b/src/components/router-provider.tsx @@ -0,0 +1,22 @@ +/* eslint-disable simple-import-sort/imports */ +import React from 'react' +import { MemoryRouter, Routes, Route } from 'react-router-dom' +import { Home } from '@/popup/pages/Home' +import { Settings } from '@/popup/pages/Settings' +import { MainLayout } from './layout/main-layout' + +export const ROUTES = { + INDEX: 'index', + SETTINGS: 'settings', +} + +export const RouterProvider = () => ( + + + }> + } /> + } /> + + + +) diff --git a/src/popup/Popup.scss b/src/popup/Popup.scss index 5dcf9432..da855dc0 100644 --- a/src/popup/Popup.scss +++ b/src/popup/Popup.scss @@ -3,224 +3,218 @@ body { height: fit-content; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', - 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; } -.wrapper { +.spentAmount { + color: #999; + font-size: 12px; + letter-spacing: -0.5px; + font-weight: bold; + position: absolute; + right: 20px; + top: 40px; + + span { + color: #6adaab; + } +} + +* { + box-sizing: border-box; +} + +.content { display: flex; flex-direction: column; - font-size: 16px; + align-items: center; + justify-content: center; + min-height: 192px; + flex-basis: auto; + padding: 0 20px 10px; + + img { + height: 96px; + } +} - .spentAmount { - color: #999; - font-size: 12px; - letter-spacing: -0.5px; - font-weight: bold; - position: absolute; - right: 20px; - top: 40px; +.pointerForm { + width: 100%; + border-radius: 8px; + border: 1px solid #e0e0e0; + overflow: hidden; + height: 100px; + display: grid; + grid-auto-flow: row; + grid-auto-rows: 1fr; + position: relative; + padding-right: 60px; - span { - color: #6adaab; + &.active { + &::before { + content: ''; + position: absolute; + background: rgb(255 255 255 / 70%); + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1; } } - * { - box-sizing: border-box; - } - - .content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 192px; - flex-basis: auto; - padding: 0 20px 10px; + .input-wrapper { + position: relative; + border-bottom: 1px solid #e0e0e0; - img { - height: 96px; + &:nth-child(2) { + border: 0; } - } - .pointerForm { - width: 100%; - border-radius: 8px; - border: 1px solid #e0e0e0; - overflow: hidden; - height: 100px; - display: grid; - grid-auto-flow: row; - grid-auto-rows: 1fr; - position: relative; - padding-right: 60px; - - &.active { - &::before { - content: ''; - position: absolute; - background: rgb(255 255 255 / 70%); - width: 100%; - height: 100%; - top: 0; - left: 0; - z-index: 1; - } + label { + color: #666; + font-size: 10px; + display: block; + position: absolute; + font-weight: 600; + text-transform: uppercase; + letter-spacing: -0.5px; + pointer-events: none; + top: 0; + left: 0; + padding: 7px 10px; } - .input-wrapper { - position: relative; - border-bottom: 1px solid #e0e0e0; + .input { + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: center; + height: 100%; + padding-top: 14px; + + &.input-disabled { + background-color: #f5f5f5; - &:nth-child(2) { - border: 0; + .edit-btn { + background-color: #e0e0e0; + } } - label { - color: #666; - font-size: 10px; + > img { display: block; - position: absolute; - font-weight: 600; - text-transform: uppercase; - letter-spacing: -0.5px; - pointer-events: none; - top: 0; - left: 0; - padding: 7px 10px; + width: 16px; + height: 16px; + align-self: center; + transform: translateX(10px); } - .input { - display: flex; - flex-direction: row; - align-items: flex-end; - justify-content: center; - height: 100%; - padding-top: 14px; - - &.input-disabled { - background-color: #f5f5f5; - - .edit-btn { - background-color: #e0e0e0; - } + &.input-disabled { + input { + pointer-events: none; } + } + } + } - > img { - display: block; - width: 16px; - height: 16px; - align-self: center; - transform: translateX(10px); - } + input { + width: 100%; + height: 100%; + border: 0; + padding: 0 10px; + box-sizing: border-box; + background-color: transparent; + color: black; + font-size: 14px; - &.input-disabled { - input { - pointer-events: none; - } - } - } + &:focus { + outline: none; } - input { - width: 100%; - height: 100%; - border: 0; - padding: 0 10px; - box-sizing: border-box; - background-color: transparent; - color: black; - font-size: 14px; - - &:focus { - outline: none; - } + &::placeholder { + color: #aaa; + } + } - &::placeholder { - color: #aaa; - } + button { + cursor: pointer; + width: 100%; + height: 100%; + border: 0; + font-weight: 500; + color: white; + text-transform: uppercase; + background-color: #6adaab; + display: flex; + align-items: center; + justify-content: center; + + img { + display: block; + width: 16px; + height: 16px; } + } + + .actions { + background-color: white; + position: absolute; + right: 0; + top: 0; + bottom: 0; + z-index: 2; + display: grid; + grid-gap: 2px; + grid-auto-flow: row; + grid-auto-rows: 1fr; + padding: 4px 4px 4px 16px; button { - cursor: pointer; - width: 100%; - height: 100%; - border: 0; - font-weight: 500; - color: white; - text-transform: uppercase; - background-color: #6adaab; - display: flex; - align-items: center; - justify-content: center; + width: 44px; + padding: 0; - img { - display: block; - width: 16px; - height: 16px; - } - } + &.submit-btn { + border-radius: 0 6px 6px 0; + position: relative; - .actions { - background-color: white; - position: absolute; - right: 0; - top: 0; - bottom: 0; - z-index: 2; - display: grid; - grid-gap: 2px; - grid-auto-flow: row; - grid-auto-rows: 1fr; - padding: 4px 4px 4px 16px; - - button { - width: 44px; - padding: 0; - - &.submit-btn { - border-radius: 0 6px 6px 0; - position: relative; - - &.loading { - img { - display: none; - } - - &::before { - content: ''; - width: 20px; - height: 20px; - border: 3px solid white; - border-top-color: rgb(255 255 255 / 20%); - border-left-color: rgb(255 255 255 / 20%); - animation: spin 500ms linear infinite; - border-radius: 50%; - } + &.loading { + img { + display: none; + } + + &::before { + content: ''; + width: 20px; + height: 20px; + border: 3px solid white; + border-top-color: rgb(255 255 255 / 20%); + border-left-color: rgb(255 255 255 / 20%); + animation: spin 500ms linear infinite; + border-radius: 50%; } } + } - &.stop-btn { - background-color: black; - border-radius: 0 6px 6px 0; + &.stop-btn { + background-color: black; + border-radius: 0 6px 6px 0; - img { - height: 32px; - width: 32px; - } + img { + height: 32px; + width: 32px; } + } - &.edit-btn { - display: none; - border-radius: 0 6px 0 0; + &.edit-btn { + display: none; + border-radius: 0 6px 0 0; - &.active { - display: block; - } + &.active { + display: block; } } } diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index f8b8d062..504aacdc 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,168 +1,11 @@ -import './Popup.scss' - -import React, { useEffect, useState } from 'react' -import { runtime } from 'webextension-polyfill' +import React from 'react' -import PopupFooter from '@/components/Popup/PopupFooter' -import PopupHeader from '@/components/Popup/PopupHeader' -import { Switch } from '@/components/switch' -import { sendMessage, sendMessageToActiveTab } from '@/utils/sendMessages' +import './Popup.scss' -const Success = runtime.getURL('assets/images/web-monetization-success.svg') -const Fail = runtime.getURL('assets/images/web-monetization-fail.svg') -const CheckIcon = runtime.getURL('assets/images/check.svg') -const DollarIcon = runtime.getURL('assets/images/dollar.svg') -const CloseIcon = runtime.getURL('assets/images/close.svg') +import { RouterProvider } from '@/components/router-provider' const Popup = () => { - const [loading, setLoading] = useState(false) - const [paymentStarted, setPaymentStarted] = useState(false) - const [spent, setSpent] = useState(0) - const [sendingPaymentPointer, setSendingPaymentPointer] = useState('') - const [isMonetizationReady, setIsMonetizationReady] = useState(false) - const [receivingPaymentPointer, setReceivingPaymentPointer] = useState('') - const [formData, setFormData] = useState({ - paymentPointer: sendingPaymentPointer || '', - amount: 20, - }) - - useEffect(() => { - checkMonetizationReady() - getSendingPaymentPointer() - listenForIncomingPayment() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const checkMonetizationReady = async () => { - const response = await sendMessageToActiveTab({ type: 'IS_MONETIZATION_READY' }) - setIsMonetizationReady(response.data.monetization) - setReceivingPaymentPointer(response.data.paymentPointer) - } - - const handleChange = (event: React.ChangeEvent) => { - setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value })) - } - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() - - setLoading(true) - const data = { - amount: formData.amount, - paymentPointer: formData.paymentPointer, - incomingPayment: receivingPaymentPointer, - } - - await sendMessage({ type: 'SET_INCOMING_POINTER', data }) - } - - const getSendingPaymentPointer = async () => { - const response = await sendMessage({ type: 'GET_SENDING_PAYMENT_POINTER' }) - setSendingPaymentPointer(response.data.sendingPaymentPointerUrl) - - const { sendingPaymentPointerUrl: paymentPointer, amount } = response.data - if (paymentPointer && amount) { - setFormData({ - paymentPointer: response.data.sendingPaymentPointerUrl, - amount: response.data.amount, - }) - } - } - - const listenForIncomingPayment = async () => { - const listener = (message: any) => { - if (message.type === 'SPENT_AMOUNT') { - setSpent(message.data.spentAmount) - setPaymentStarted(true) - } - - if (loading) { - setLoading(false) - } - } - - runtime.onMessage.addListener(listener) - return () => { - runtime.onMessage.removeListener(listener) - } - } - - const stopPayments = async (e: React.MouseEvent) => { - e.preventDefault() - setPaymentStarted(false) - setTimeout(() => { - if (loading) { - setLoading(false) - } - }, 1000) - await sendMessageToActiveTab({ type: 'STOP_PAYMENTS' }) - } - - return ( -
- - {!!spent && ( -
- ${spent}/$20 -
- )} -
- {isMonetizationReady ? ( - <> - Success - -
-
- -
- -
-
- -
- -
- dollar - -
-
- -
- {paymentStarted ? ( - - ) : ( - - )} -
-
- - ) : ( - Fail - )} -
- -
- ) + return } export default Popup diff --git a/src/popup/pages/Home.tsx b/src/popup/pages/Home.tsx new file mode 100644 index 00000000..46a81f0e --- /dev/null +++ b/src/popup/pages/Home.tsx @@ -0,0 +1,178 @@ +import React, { useEffect, useState } from 'react' +import { runtime } from 'webextension-polyfill' + +import { sendMessage, sendMessageToActiveTab } from '@/utils/sendMessages' + +const Success = runtime.getURL('assets/images/web-monetization-success.svg') +const Fail = runtime.getURL('assets/images/web-monetization-fail.svg') +const CheckIcon = runtime.getURL('assets/images/check.svg') +const DollarIcon = runtime.getURL('assets/images/dollar.svg') +const CloseIcon = runtime.getURL('assets/images/close.svg') + +// --- Temporary code until real UI implemented --- + +interface IProps { + isMonetizationReady: boolean +} + +const PopupFooter: React.FC = ({ isMonetizationReady }) => ( +
+ {isMonetizationReady ? ( + This site is Web Monetization ready + ) : ( + This site isn't Web Monetization ready + )} +
+) + +// --- End of Temporary code until real UI implemented --- + +export const Home = () => { + const [loading, setLoading] = useState(false) + const [paymentStarted, setPaymentStarted] = useState(false) + const [spent, setSpent] = useState(0) + const [sendingPaymentPointer, setSendingPaymentPointer] = useState('') + const [isMonetizationReady, setIsMonetizationReady] = useState(false) + const [receivingPaymentPointer, setReceivingPaymentPointer] = useState('') + const [formData, setFormData] = useState({ + paymentPointer: sendingPaymentPointer || '', + amount: 20, + }) + + useEffect(() => { + checkMonetizationReady() + getSendingPaymentPointer() + listenForIncomingPayment() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const checkMonetizationReady = async () => { + const response = await sendMessageToActiveTab({ type: 'IS_MONETIZATION_READY' }) + setIsMonetizationReady(response.data.monetization) + setReceivingPaymentPointer(response.data.paymentPointer) + } + + const handleChange = (event: React.ChangeEvent) => { + setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value })) + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + + setLoading(true) + const data = { + amount: formData.amount, + paymentPointer: formData.paymentPointer, + incomingPayment: receivingPaymentPointer, + } + + await sendMessage({ type: 'SET_INCOMING_POINTER', data }) + } + + const getSendingPaymentPointer = async () => { + const response = await sendMessage({ type: 'GET_SENDING_PAYMENT_POINTER' }) + setSendingPaymentPointer(response.data.sendingPaymentPointerUrl) + + const { sendingPaymentPointerUrl: paymentPointer, amount } = response.data + if (paymentPointer && amount) { + setFormData({ + paymentPointer: response.data.sendingPaymentPointerUrl, + amount: response.data.amount, + }) + } + } + + const listenForIncomingPayment = async () => { + const listener = (message: any) => { + if (message.type === 'SPENT_AMOUNT') { + setSpent(message.data.spentAmount) + setPaymentStarted(true) + } + + if (loading) { + setLoading(false) + } + } + + runtime.onMessage.addListener(listener) + return () => { + runtime.onMessage.removeListener(listener) + } + } + + const stopPayments = async (e: React.MouseEvent) => { + e.preventDefault() + setPaymentStarted(false) + setTimeout(() => { + if (loading) { + setLoading(false) + } + }, 1000) + await sendMessageToActiveTab({ type: 'STOP_PAYMENTS' }) + } + + return ( + <> + {!!spent && ( +
+ ${spent}/$20 +
+ )} +
+ {isMonetizationReady ? ( + <> + Success + +
+
+ +
+ +
+
+ +
+ +
+ dollar + +
+
+ +
+ {paymentStarted ? ( + + ) : ( + + )} +
+
+ + ) : ( + Fail + )} +
+ + + ) +} diff --git a/src/popup/pages/Settings.tsx b/src/popup/pages/Settings.tsx new file mode 100644 index 00000000..eacdd66a --- /dev/null +++ b/src/popup/pages/Settings.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export const Settings = () => { + return <>Settings +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 6c7d30e1..a04b5c2e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -35,6 +35,10 @@ module.exports = { focus: 'rgb(var(--border-focus) / )', error: 'rgb(var(--border-error) / )', }, + backgroundImage: { + 'divider-gradient': + 'linear-gradient(90deg, #FF7A7F 0%, #FF7A7F 0%, #FF7A7F 14.3%, #56B7B5 14.3%, #56B7B5 28.6%, #56B7B5 28.6%, #A3BEDC 28.6%, #A3BEDC 42.9%, #A3BEDC 42.9%, #FFC8DC 42.9%, #FFC8DC 57.2%, #FFC8DC 57.2%, #FF9852 57.2%, #FF9852 71.5%, #FF9852 71.5%, #98E1D0 71.5%, #98E1D0 85.8%, #98E1D0 85.8%, #8075B3 85.8%, #8075B3 100%, #8075B3 100%)', + }, }, }, plugins: [],