diff --git a/src/background/Background.ts b/src/background/Background.ts index 9acfbe7b..014bb507 100644 --- a/src/background/Background.ts +++ b/src/background/Background.ts @@ -5,11 +5,15 @@ import { type PaymentFlowService } from '@/background/paymentFlow' import { exportJWK, generateEd25519KeyPair } from '@/utils/crypto' import { defaultData, storageApi } from '@/utils/storage' -import getSendingPaymentPointerHandler from '../messageHandlers/getSendingPaymentPointerHandler' -import getStorageData from '../messageHandlers/getStorageData' -import isMonetizationReadyHandler from '../messageHandlers/isMonetizationReadyHandler' -import runPaymentHandler from '../messageHandlers/runPaymentHandler' -import setIncomingPointerHandler from '../messageHandlers/setIncomingPointerHandler' +import { + getSendingPaymentPointerHandler, + getStorageData, + getStorageKey, + isMonetizationReadyHandler, + runPaymentHandler, + setIncomingPointerHandler, + setStorageKey, +} from '../messageHandlers' import { tabChangeHandler, tabUpdateHandler } from './tabHandlers' class Background { @@ -19,6 +23,8 @@ class Background { getSendingPaymentPointerHandler, runPaymentHandler, getStorageData, + getStorageKey, + setStorageKey, ] private subscriptions: any = [] // TO DO: remove these from background into storage or state & use injection @@ -27,10 +33,16 @@ class Background { paymentStarted = false constructor() { - storageApi - .set({ data: defaultData }) - .then(() => console.log('Default data stored successfully')) - .catch((error: any) => console.error('Error storing data:', error)) + this.setStorageDefaultData() + } + + // TODO: to be moved to a service + async setStorageDefaultData() { + try { + await storageApi.set({ ...defaultData }) + } catch (error) { + console.error('Error storing data:', error) + } } subscribeToMessages() { diff --git a/src/components/__tests__/slider.test.tsx b/src/components/__tests__/slider.test.tsx new file mode 100644 index 00000000..7c7798d3 --- /dev/null +++ b/src/components/__tests__/slider.test.tsx @@ -0,0 +1,30 @@ +import '@testing-library/jest-dom' + +import { render, screen } from '@testing-library/react' +import React from 'react' + +import { Slider } from '../slider' + +describe('Slider Component', () => { + it('renders without crashing', () => { + render(<Slider />) + expect(screen.getByRole('slider')).toBeInTheDocument() + }) + + it('handles disabled prop', () => { + render(<Slider disabled={true} />) + expect(screen.getByRole('slider')).toBeDisabled() + }) + + it('displays error message when provided', () => { + const errorMessage = 'Error message' + render(<Slider errorMessage={errorMessage} />) + expect(screen.getByText(errorMessage)).toBeInTheDocument() + }) + + it('passes additional props to the input', () => { + const testName = 'test-name' + render(<Slider name={testName} />) + expect(screen.getByRole('slider')).toHaveAttribute('name', testName) + }) +}) diff --git a/src/components/slider.tsx b/src/components/slider.tsx index 2e8b454f..6981e7a7 100644 --- a/src/components/slider.tsx +++ b/src/components/slider.tsx @@ -9,47 +9,44 @@ export interface SliderProps extends React.InputHTMLAttributes<HTMLInputElement> min?: number max?: number value?: number - defaultValue?: number + onChange?: (_event: React.ChangeEvent<HTMLInputElement>) => void } +const sliderClasses = ` + [&::-webkit-slider-thumb]:appearance-none + [&::-webkit-slider-thumb]:w-5 + [&::-webkit-slider-thumb]:h-5 + [&::-webkit-slider-thumb]:bg-switch-base + [&::-webkit-slider-thumb]:rounded-full + [&::-moz-range-thumb]:appearance-none + [&::-moz-range-thumb]:w-5 + [&::-moz-range-thumb]:h-5 + [&::-moz-range-thumb]:bg-switch-base + [&::-moz-range-thumb]:rounded-full + w-full h-1 bg-disabled-strong rounded-lg + appearance-none cursor-pointer dark:bg-disabled-strong +` + export const Slider = forwardRef<HTMLInputElement, SliderProps>(function Slider( - { errorMessage, defaultValue, value, className, disabled, ...props }, + { errorMessage, value = 0, className, onChange = () => {}, disabled, ...props }, ref, ) { - const [innerValue, setInnerValue] = React.useState<number>(value || defaultValue || 0) - return ( - <div className="w-100"> - <input - ref={ref} - type="range" - className={ - (cn( - `[&::-webkit-slider-thumb]:appearance-none - [&::-webkit-slider-thumb]:w-5 - [&::-webkit-slider-thumb]:h-5 - [&::-webkit-slider-thumb]:bg-switch-base - [&::-webkit-slider-thumb]:rounded-full - [&::-moz-range-thumb]:appearance-none - [&::-moz-range-thumb]:w-5 - [&::-moz-range-thumb]:h-5 - [&::-moz-range-thumb]:bg-switch-base - [&::-moz-range-thumb]:rounded-full - w-full h-1 bg-disabled-strong rounded-lg appearance-none cursor-pointer dark:bg-disabled-strong`, - innerValue === 0 && - '[&::-webkit-slider-thumb]:bg-disabled-strong [&::-moz-range-thumb]:bg-disabled-strong', - ), - className) - } - disabled={disabled ?? false} - aria-disabled={disabled ?? false} - aria-invalid={!!errorMessage} - aria-describedby={errorMessage} - defaultValue={defaultValue} - value={innerValue} - onChange={e => setInnerValue(Number(e.target.value))} - {...props} - /> + <div className="w-full"> + <div className="h-1 flex items-center"> + <input + ref={ref} + type="range" + className={sliderClasses + cn(className)} + disabled={disabled ?? false} + aria-disabled={disabled ?? false} + aria-invalid={!!errorMessage} + aria-describedby={errorMessage} + value={value} + onChange={onChange} + {...props} + /> + </div> {errorMessage && <p className="text-error text-sm px-2">{errorMessage}</p>} </div> diff --git a/src/messageHandlers/getStorageData.ts b/src/messageHandlers/getStorageData.ts index 1152a519..d1ae9648 100644 --- a/src/messageHandlers/getStorageData.ts +++ b/src/messageHandlers/getStorageData.ts @@ -1,12 +1,17 @@ -import browser from 'webextension-polyfill' - -const storage = browser.storage.local +import { storageApi } from '@/utils/storage' const getStorageData = async () => { - const data = await storage.get('data') - return { - type: 'SUCCESS', - data, + try { + const data = await storageApi.get('data') + return { + type: 'SUCCESS', + data, + } + } catch (error) { + return { + type: 'ERROR', + error, + } } } diff --git a/src/messageHandlers/getStorageKey.ts b/src/messageHandlers/getStorageKey.ts new file mode 100644 index 00000000..a24565b5 --- /dev/null +++ b/src/messageHandlers/getStorageKey.ts @@ -0,0 +1,18 @@ +import { storageApi } from '@/utils/storage' + +const getStorageKey = async (key: any) => { + try { + const data = await storageApi.get(key) + return { + type: 'SUCCESS', + [key]: data?.[key], + } + } catch (error) { + return { + type: 'ERROR', + error, + } + } +} + +export default { callback: getStorageKey, type: 'GET_STORAGE_KEY' } diff --git a/src/messageHandlers/index.ts b/src/messageHandlers/index.ts new file mode 100644 index 00000000..83c0a511 --- /dev/null +++ b/src/messageHandlers/index.ts @@ -0,0 +1,17 @@ +import getSendingPaymentPointerHandler from './getSendingPaymentPointerHandler' +import getStorageData from './getStorageData' +import getStorageKey from './getStorageKey' +import isMonetizationReadyHandler from './isMonetizationReadyHandler' +import runPaymentHandler from './runPaymentHandler' +import setIncomingPointerHandler from './setIncomingPointerHandler' +import setStorageKey from './setStorageKey' + +export { + getSendingPaymentPointerHandler, + getStorageData, + getStorageKey, + isMonetizationReadyHandler, + runPaymentHandler, + setIncomingPointerHandler, + setStorageKey, +} diff --git a/src/messageHandlers/setStorageKey.ts b/src/messageHandlers/setStorageKey.ts new file mode 100644 index 00000000..c800488d --- /dev/null +++ b/src/messageHandlers/setStorageKey.ts @@ -0,0 +1,12 @@ +import { storageApi } from '@/utils/storage' + +const setStorageKey = async ({ key, value }: { key: string; value: any }) => { + try { + await storageApi.set({ [key]: value }) + return { type: 'SUCCESS' } + } catch (error) { + return { type: 'ERROR', error } + } +} + +export default { callback: setStorageKey, type: 'SET_STORAGE_KEY' } diff --git a/src/popup/pages/Home.tsx b/src/popup/pages/Home.tsx index a9df4810..b4907faa 100644 --- a/src/popup/pages/Home.tsx +++ b/src/popup/pages/Home.tsx @@ -1,7 +1,10 @@ import React, { useEffect, useState } from 'react' import { runtime } from 'webextension-polyfill' +import { Slider } from '@/components/slider' +import { formatCurrency } from '@/utils/formatCurrency' import { sendMessage, sendMessageToActiveTab } from '@/utils/sendMessages' +import { getStorageKey } from '@/utils/storage' const Success = runtime.getURL('assets/images/web-monetization-success.svg') const Fail = runtime.getURL('assets/images/web-monetization-fail.svg') @@ -28,6 +31,8 @@ const PopupFooter: React.FC<IProps> = ({ isMonetizationReady }) => ( // --- End of Temporary code until real UI implemented --- export const Home = () => { + const [remainingBalance, setRemainingBalance] = useState(0) + const [rateOfPay, setRateOfPay] = useState(0.36) const [loading, setLoading] = useState(false) const [paymentStarted, setPaymentStarted] = useState(false) const [spent, setSpent] = useState(0) @@ -36,16 +41,28 @@ export const Home = () => { const [receivingPaymentPointer, setReceivingPaymentPointer] = useState('') const [formData, setFormData] = useState({ paymentPointer: sendingPaymentPointer || '', - amount: 20, + amount: 0, }) useEffect(() => { checkMonetizationReady() getSendingPaymentPointer() listenForIncomingPayment() + getRateOfPay() + getRemainingBalance() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const getRateOfPay = async () => { + const response = await getStorageKey('rateOfPay') + response && setRateOfPay(response) + } + + const getRemainingBalance = async () => { + const response = await getStorageKey('amount') + response && setRemainingBalance(response) + } + const checkMonetizationReady = async () => { const response = await sendMessageToActiveTab({ type: 'IS_MONETIZATION_READY' }) setIsMonetizationReady(response.data.monetization) @@ -71,7 +88,6 @@ export const Home = () => { const getSendingPaymentPointer = async () => { const response = await sendMessage({ type: 'GET_SENDING_PAYMENT_POINTER' }) - console.log('getSendingPaymentPointer', response) setSendingPaymentPointer(response.data.sendingPaymentPointerUrl) const { sendingPaymentPointerUrl: paymentPointer, amount } = response.data @@ -112,6 +128,14 @@ export const Home = () => { await sendMessageToActiveTab({ type: 'STOP_PAYMENTS' }) } + const updateRateOfPay = async (event: any) => { + setRateOfPay(event.target.value) + await sendMessage({ + type: 'SET_STORAGE_KEY', + data: { key: 'rateOfPay', value: event.target.value }, + }) + } + return ( <> {!!spent && ( @@ -120,6 +144,15 @@ export const Home = () => { </div> )} <div className="content"> + <div className="grid gap-4 w-full"> + <div className="px-2 text-base font-medium text-medium">Current rate of pay</div> + <Slider min={0} max={1} step={0.01} value={rateOfPay} onChange={updateRateOfPay} /> + <div className="flex items-center justify-between w-full"> + <span>{formatCurrency(rateOfPay)} per hour</span> + <span>Remaining balance: ${remainingBalance}</span> + </div> + </div> + {isMonetizationReady ? ( <> <img src={Success} alt="Success" /> diff --git a/src/providers/__tests__/popup-context.test.tsx b/src/providers/__tests__/popup-context.test.tsx index 63fbe7e5..d5b80640 100644 --- a/src/providers/__tests__/popup-context.test.tsx +++ b/src/providers/__tests__/popup-context.test.tsx @@ -1,7 +1,9 @@ import { act, render, screen } from '@testing-library/react' import React, { useContext } from 'react' -import { defaultData, PopupContext, PopupProvider } from '../popup.provider' +import { defaultData } from '@/utils/storage' + +import { PopupContext, PopupProvider } from '../popup.provider' jest.mock('webextension-polyfill', () => ({ runtime: { diff --git a/src/providers/popup.provider.tsx b/src/providers/popup.provider.tsx index 4f2e10b6..999e77ae 100644 --- a/src/providers/popup.provider.tsx +++ b/src/providers/popup.provider.tsx @@ -1,43 +1,35 @@ import React, { createContext, useEffect, useState } from 'react' -import { getStorageData } from '@/utils/storage' +import { defaultData, getStorageData } from '@/utils/storage' import { PopupContextValue, TPopupContext } from './providers.interface' -export const defaultData = { - connected: false, - wallet: '', - amount: 0, - amountType: { - recurring: true, - }, - rateOfPay: 0.36, - wmEnabled: true, - accessTokenQuote: '', - accessTokenOutgoing: '', - refreshToken: '', - manageUrl: '', -} - interface IProps { children: React.ReactNode } export const PopupContext = createContext<PopupContextValue>({ - data: defaultData, + data: { ...defaultData }, setData: () => {}, }) export const PopupProvider: React.FC<IProps> = ({ children }) => { - const [data, setData] = useState<TPopupContext>(defaultData) + const [data, setData] = useState<TPopupContext>({ ...defaultData }) useEffect(() => { - ;(async () => { - const storageData = await getStorageData() - setData(storageData as TPopupContext) - })() - - // eslint-disable-next-line react-hooks/exhaustive-deps + const fetchData = async () => { + try { + const storageData = await getStorageData() + if (storageData) { + setData(prevState => ({ ...prevState, ...storageData })) + } + } catch (error) { + console.error('Error fetching storage data:', error) + setData(defaultData) + } + } + + fetchData() }, []) return <PopupContext.Provider value={{ data, setData }}>{children}</PopupContext.Provider> diff --git a/src/types/message.d.ts b/src/types/message.d.ts index 1fe988d8..2cecc84b 100644 --- a/src/types/message.d.ts +++ b/src/types/message.d.ts @@ -13,6 +13,8 @@ declare type EXTMessageType = | 'LOAD' | 'GET_STORAGE_DATA' | 'SET_STORAGE_DATA' + | 'GET_STORAGE_KEY' + | 'SET_STORAGE_KEY' declare type EXTMessage<T = any> = { type: EXTMessageType diff --git a/src/utils/formatCurrency.ts b/src/utils/formatCurrency.ts new file mode 100644 index 00000000..e8bef4a1 --- /dev/null +++ b/src/utils/formatCurrency.ts @@ -0,0 +1,7 @@ +export const formatCurrency = (value: any): string => { + if (value < 1) { + return `${Math.round(value * 100)}c` + } else { + return `$${parseFloat(value).toFixed(2)}` + } +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 949b2a03..569df536 100755 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -3,20 +3,9 @@ import browser from 'webextension-polyfill' import { TPopupContext } from '@/providers/providers.interface' import { sendMessage } from '@/utils/sendMessages' -export interface ExtensionStorageData { - amount: number - amountType: { - recurring: boolean - } - rateOfPay: number - wmEnabled: boolean - accessTokenQuote: string - accessTokenOutgoing: string - refreshToken: string - manageUrl: string -} - -export const defaultData: ExtensionStorageData = { +export const defaultData: TPopupContext = { + connected: false, + wallet: '', amount: 0, amountType: { recurring: true, @@ -39,4 +28,9 @@ export const getStorageData = async () => { } } +export const getStorageKey = async (key: string) => { + const response: any = await sendMessage({ type: 'GET_STORAGE_KEY', data: key }) + return response?.[key] +} + export const storageApi = browser.storage?.sync || browser.storage?.local