Skip to content

Commit

Permalink
Merge branch 'main' into rp--open-payments-sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
raducristianpopa committed Feb 26, 2024
2 parents 17ba4c5 + cecc535 commit a52651a
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 92 deletions.
30 changes: 21 additions & 9 deletions src/background/Background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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() {
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
19 changes: 12 additions & 7 deletions src/messageHandlers/getStorageData.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/messageHandlers/getStorageKey.ts
Original file line number Diff line number Diff line change
@@ -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' }
17 changes: 17 additions & 0 deletions src/messageHandlers/index.ts
Original file line number Diff line number Diff line change
@@ -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,
}
12 changes: 12 additions & 0 deletions src/messageHandlers/setStorageKey.ts
Original file line number Diff line number Diff line change
@@ -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' }
37 changes: 35 additions & 2 deletions 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 @@ -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)
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,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 && (
Expand All @@ -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" />
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
Loading

0 comments on commit a52651a

Please sign in to comment.