Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Rate of pay slider component update #103

Merged
merged 6 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions src/background/Background.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import browser, { Runtime, runtime, tabs } from 'webextension-polyfill'
import { Runtime, runtime, storage as storageApi, tabs } from 'webextension-polyfill'

import { PaymentFlowService } from '@/background/grantFlow'
import { defaultData } from '@/utils/storage'

import getSendingPaymentPointerHandler from '../messageHandlers/getSendingPaymentPointerHandler'
import getStorageData from '../messageHandlers/getStorageData'
import isMonetizationReadyHandler from '../messageHandlers/isMonetizationReadyHandler'
import setIncomingPointerHandler from '../messageHandlers/setIncomingPointerHandler'
import {
getSendingPaymentPointerHandler,
getStorageData,
getStorageKey,
isMonetizationReadyHandler,
setIncomingPointerHandler,
setStorageKey,
} from '../messageHandlers'
import { tabChangeHandler, tabUpdateHandler } from './tabHandlers'

const storage = browser.storage.local
const storage = storageApi.sync || storageApi.local

class Background {
private messageHandlers: any = [
isMonetizationReadyHandler,
setIncomingPointerHandler,
getSendingPaymentPointerHandler,
getStorageData,
getStorageKey,
setStorageKey,
]
private subscriptions: any = []
// TO DO: remove these from background into storage or state & use injection
Expand All @@ -25,18 +31,23 @@ class Background {
paymentStarted = false

constructor() {
storage
.set({ data: defaultData })
.then(() => console.log('Default data stored successfully'))
.catch((error: any) => console.error('Error storing data:', error))
this.setStorageDefaultData()
}

async setStorageDefaultData() {
try {
dianafulga marked this conversation as resolved.
Show resolved Hide resolved
await storage.set({ data: defaultData })
} catch (error) {
console.error('Error storing data:', error)
}
}

subscribeToMessages() {
this.subscriptions = this.messageHandlers.map((handler: any) => {
const listener: any = (
message: EXTMessage,
sender: Runtime.MessageSender,
sendResponse: (res: any) => void,
sendResponse: (_res: any) => void,
) => {
if (handler.type === message.type) {
handler
Expand Down
30 changes: 30 additions & 0 deletions src/components/__tests__/slider.test.tsx
Original file line number Diff line number Diff line change
@@ -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)
})
})
67 changes: 32 additions & 35 deletions src/components/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion src/messageHandlers/getStorageData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import browser from 'webextension-polyfill'

const storage = browser.storage.local
const storage = browser.storage.sync || browser.storage.local
dianafulga marked this conversation as resolved.
Show resolved Hide resolved

const getStorageData = async () => {
const data = await storage.get('data')
dianafulga marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
13 changes: 13 additions & 0 deletions src/messageHandlers/getStorageKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import browser from 'webextension-polyfill'

const storage = browser.storage.sync || browser.storage.local

const getStorageKey = async (key: any) => {
const data = await storage.get('data')
dianafulga marked this conversation as resolved.
Show resolved Hide resolved
return {
type: 'SUCCESS',
[key]: data?.data[key],
}
}

export default { callback: getStorageKey, type: 'GET_STORAGE_KEY' }
15 changes: 15 additions & 0 deletions src/messageHandlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import getSendingPaymentPointerHandler from './getSendingPaymentPointerHandler'
import getStorageData from './getStorageData'
import getStorageKey from './getStorageKey'
import isMonetizationReadyHandler from './isMonetizationReadyHandler'
import setIncomingPointerHandler from './setIncomingPointerHandler'
import setStorageKey from './setStorageKey'

export {
getSendingPaymentPointerHandler,
getStorageData,
getStorageKey,
isMonetizationReadyHandler,
setIncomingPointerHandler,
setStorageKey,
}
10 changes: 10 additions & 0 deletions src/messageHandlers/setStorageKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import browser from 'webextension-polyfill'

const storage = browser.storage.sync || browser.storage.local

const getStorageKey = async ({ key, value }: { key: string; value: any }) => {
dianafulga marked this conversation as resolved.
Show resolved Hide resolved
await storage.set({ [key]: value })
return { type: 'SUCCESS' }
}

export default { callback: getStorageKey, type: 'SET_STORAGE_KEY' }
32 changes: 31 additions & 1 deletion src/popup/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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)
Expand All @@ -43,9 +48,21 @@ export const Home = () => {
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)
Expand All @@ -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
Expand Down Expand Up @@ -112,6 +128,11 @@ 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 } })
}

return (
<>
{!!spent && (
Expand All @@ -120,6 +141,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" />
Expand Down
4 changes: 3 additions & 1 deletion src/providers/__tests__/popup-context.test.tsx
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
40 changes: 16 additions & 24 deletions src/providers/popup.provider.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
2 changes: 2 additions & 0 deletions src/types/message.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/utils/formatCurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const formatCurrency = (value: any): string => {
if (value < 1) {
return `${Math.round(value * 100)}¢`
} else {
return `$${parseFloat(value).toFixed(2)}`
}
}
Loading
Loading