Merge pull request #192 from ekzyis/103-add-other-currencies

Support other currencies
This commit is contained in:
Keyan 2022-10-04 14:54:57 -05:00 committed by GitHub
commit 0ff9bbc92d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 75 additions and 24 deletions

View File

@ -43,7 +43,7 @@ export function getGetServerSideProps (query, variables = null, notFoundFunc, re
query: ME_SSR query: ME_SSR
}) })
const price = await getPrice() const price = await getPrice(me?.fiatCurrency)
// we want to use client-side cache // we want to use client-side cache
if (nodata && query) { if (nodata && query) {

View File

@ -29,7 +29,7 @@ export default gql`
extend type Mutation { extend type Mutation {
setName(name: String!): Boolean setName(name: String!): Boolean
setSettings(tipDefault: Int!, noteItemSats: Boolean!, noteEarning: Boolean!, setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!,
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!): User wildWestMode: Boolean!, greeterMode: Boolean!): User
@ -59,6 +59,7 @@ export default gql`
hasNewNotes: Boolean! hasNewNotes: Boolean!
hasInvites: Boolean! hasInvites: Boolean!
tipDefault: Int! tipDefault: Int!
fiatCurrency: String!
bio: Item bio: Item
bioId: Int bioId: Int
photoId: Int photoId: Int

View File

@ -6,7 +6,7 @@ import { Formik, Form as FormikForm, useFormikContext, useField, FieldArray } fr
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import copy from 'clipboard-copy' import copy from 'clipboard-copy'
import Thumb from '../svgs/thumb-up-fill.svg' import Thumb from '../svgs/thumb-up-fill.svg'
import { Col, Dropdown, Nav } from 'react-bootstrap' import { Col, Dropdown as BootstrapDropdown, Nav } from 'react-bootstrap'
import Markdown from '../svgs/markdown-line.svg' import Markdown from '../svgs/markdown-line.svg'
import styles from './form.module.css' import styles from './form.module.css'
import Text from '../components/text' import Text from '../components/text'
@ -269,10 +269,10 @@ export function InputUserSuggest ({ label, groupClassName, ...props }) {
} }
}} }}
/> />
<Dropdown show={suggestions.array.length > 0}> <BootstrapDropdown show={suggestions.array.length > 0}>
<Dropdown.Menu className={styles.suggestionsMenu}> <BootstrapDropdown.Menu className={styles.suggestionsMenu}>
{suggestions.array.map((v, i) => {suggestions.array.map((v, i) =>
<Dropdown.Item <BootstrapDropdown.Item
key={v.name} key={v.name}
active={suggestions.index === i} active={suggestions.index === i}
onClick={() => { onClick={() => {
@ -281,9 +281,9 @@ export function InputUserSuggest ({ label, groupClassName, ...props }) {
}} }}
> >
{v.name} {v.name}
</Dropdown.Item>)} </BootstrapDropdown.Item>)}
</Dropdown.Menu> </BootstrapDropdown.Menu>
</Dropdown> </BootstrapDropdown>
</FormGroup> </FormGroup>
) )
} }
@ -429,3 +429,21 @@ export function SyncForm ({
</Formik> </Formik>
) )
} }
export function Dropdown ({ label, items, groupClassName, ...props }) {
const [field, _, helper] = useField({ ...props, type: 'input' })
return (
<FormGroup label={label} className={groupClassName}>
<BootstrapDropdown>
<BootstrapDropdown.Toggle>
{field.value}
</BootstrapDropdown.Toggle>
<BootstrapDropdown.Menu>
{items.map(item => (
<BootstrapDropdown.Item onSelect={() => helper.setValue(item)}>{item}</BootstrapDropdown.Item>
))}
</BootstrapDropdown.Menu>
</BootstrapDropdown>
</FormGroup>
)
}

View File

@ -9,7 +9,7 @@ import styles from '../styles/post.module.css'
import { useLazyQuery, gql, useMutation } from '@apollo/client' import { useLazyQuery, gql, useMutation } from '@apollo/client'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Link from 'next/link' import Link from 'next/link'
import { usePrice } from './price' import { CURRENCY_SYMBOLS, usePrice } from './price'
import Avatar from './avatar' import Avatar from './avatar'
import BootstrapForm from 'react-bootstrap/Form' import BootstrapForm from 'react-bootstrap/Form'
import Alert from 'react-bootstrap/Alert' import Alert from 'react-bootstrap/Alert'
@ -36,13 +36,16 @@ function satsMin2Mo (minute) {
function PriceHint ({ monthly }) { function PriceHint ({ monthly }) {
const price = usePrice() const price = usePrice()
const { fiatCurrency } = useMe();
const fiatSymbol = CURRENCY_SYMBOLS[fiatCurrency]
if (!price || !monthly) { if (!price || !monthly) {
return null return null
} }
const fixed = (n, f) => Number.parseFloat(n).toFixed(f) const fixed = (n, f) => Number.parseFloat(n).toFixed(f)
const fiat = fixed((price / 100000000) * monthly, 0) const fiat = fixed((price / 100000000) * monthly, 0)
return <span className='text-muted'>{monthly} sats/mo which is ${fiat}/mo</span> return <span className='text-muted'>{monthly} sats/mo which is {fiatSymbol}{fiat}/mo</span>
} }
// need to recent list items // need to recent list items

View File

@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import useSWR from 'swr' import useSWR from 'swr'
import { fixedDecimal } from '../lib/format' import { fixedDecimal } from '../lib/format'
import { useMe } from './me'
const fetcher = url => fetch(url).then(res => res.json()).catch() const fetcher = url => fetch(url).then(res => res.json()).catch()
@ -9,16 +10,26 @@ export const PriceContext = React.createContext({
price: null price: null
}) })
const ENDPOINT = 'https://api.coinbase.com/v2/prices/BTC-USD/spot' export const CURRENCY_SYMBOLS = {
'AUD': '$',
'CAD': '$',
'EUR': '€',
'GBP': '£',
'USD': '$',
'NZD': '$'
}
export async function getPrice () { const endpoint = (fiat) => `https://api.coinbase.com/v2/prices/BTC-${fiat ?? 'USD'}/spot`
const data = await fetcher(ENDPOINT)
export async function getPrice (fiat) {
const data = await fetcher(endpoint(fiat))
return data?.data?.amount return data?.data?.amount
} }
export function PriceProvider ({ price, children }) { export function PriceProvider ({ price, children }) {
const me = useMe()
const { data } = useSWR( const { data } = useSWR(
ENDPOINT, endpoint(me?.fiatCurrency),
fetcher, fetcher,
{ {
refreshInterval: 30000 refreshInterval: 30000
@ -45,8 +56,9 @@ export default function Price () {
useEffect(() => { useEffect(() => {
setAsSats(localStorage.getItem('asSats')) setAsSats(localStorage.getItem('asSats'))
}, []) }, [])
const price = usePrice() const price = usePrice()
const me = useMe()
const fiatSymbol = CURRENCY_SYMBOLS[me?.fiatCurrency || 'USD'];
if (!price) return null if (!price) return null
@ -66,7 +78,7 @@ export default function Price () {
if (asSats === 'yep') { if (asSats === 'yep') {
return ( return (
<Button className='text-reset p-0' onClick={handleClick} variant='link'> <Button className='text-reset p-0' onClick={handleClick} variant='link'>
{fixedDecimal(100000000 / price, 0) + ' sats/$'} {fixedDecimal(100000000 / price, 0) + ` sats/${fiatSymbol}`}
</Button> </Button>
) )
} }
@ -81,7 +93,7 @@ export default function Price () {
return ( return (
<Button className='text-reset p-0' onClick={handleClick} variant='link'> <Button className='text-reset p-0' onClick={handleClick} variant='link'>
{'$' + fixedDecimal(price, 0)} {fiatSymbol + fixedDecimal(price, 0)}
</Button> </Button>
) )
} }

View File

@ -13,6 +13,7 @@ export const ME = gql`
freeComments freeComments
hasNewNotes hasNewNotes
tipDefault tipDefault
fiatCurrency
bioId bioId
hasInvites hasInvites
upvotePopover upvotePopover
@ -41,6 +42,7 @@ export const ME_SSR = gql`
freePosts freePosts
freeComments freeComments
tipDefault tipDefault
fiatCurrency
bioId bioId
upvotePopover upvotePopover
tipPopover tipPopover
@ -61,6 +63,7 @@ export const ME_SSR = gql`
export const SETTINGS_FIELDS = gql` export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User { fragment SettingsFields on User {
tipDefault tipDefault
fiatCurrency
noteItemSats noteItemSats
noteEarning noteEarning
noteAllDescendants noteAllDescendants
@ -90,11 +93,11 @@ ${SETTINGS_FIELDS}
export const SET_SETTINGS = export const SET_SETTINGS =
gql` gql`
${SETTINGS_FIELDS} ${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $noteItemSats: Boolean!, $noteEarning: Boolean!, mutation setSettings($tipDefault: Int!, $fiatCurrency: String!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
$noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!) { $wildWestMode: Boolean!, $greeterMode: Boolean!) {
setSettings(tipDefault: $tipDefault, noteItemSats: $noteItemSats, setSettings(tipDefault: $tipDefault, fiatCurrency: $fiatCurrency, noteItemSats: $noteItemSats,
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode, noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode,

View File

@ -1,4 +1,4 @@
import { Checkbox, Form, Input, SubmitButton } from '../components/form' import { Checkbox, Form, Input, SubmitButton, Dropdown } from '../components/form'
import * as Yup from 'yup' import * as Yup from 'yup'
import { Alert, Button, InputGroup, Modal } from 'react-bootstrap' import { Alert, Button, InputGroup, Modal } from 'react-bootstrap'
import LayoutCenter from '../components/layout-center' import LayoutCenter from '../components/layout-center'
@ -12,12 +12,16 @@ import { LightningAuth } from '../components/lightning-auth'
import { SETTINGS, SET_SETTINGS } from '../fragments/users' import { SETTINGS, SET_SETTINGS } from '../fragments/users'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Info from '../components/info' import Info from '../components/info'
import { CURRENCY_SYMBOLS } from '../components/price'
export const getServerSideProps = getGetServerSideProps(SETTINGS) export const getServerSideProps = getGetServerSideProps(SETTINGS)
const supportedCurrencies = Object.keys(CURRENCY_SYMBOLS)
export const SettingsSchema = Yup.object({ export const SettingsSchema = Yup.object({
tipDefault: Yup.number().typeError('must be a number').required('required') tipDefault: Yup.number().typeError('must be a number').required('required')
.positive('must be positive').integer('must be whole') .positive('must be positive').integer('must be whole'),
fiatCurrency: Yup.string().required('required').oneOf(supportedCurrencies)
}) })
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again' const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
@ -54,6 +58,7 @@ export default function Settings ({ data: { settings } }) {
<Form <Form
initial={{ initial={{
tipDefault: settings?.tipDefault || 21, tipDefault: settings?.tipDefault || 21,
fiatCurrency: settings?.fiatCurrency || 'USD',
noteItemSats: settings?.noteItemSats, noteItemSats: settings?.noteItemSats,
noteEarning: settings?.noteEarning, noteEarning: settings?.noteEarning,
noteAllDescendants: settings?.noteAllDescendants, noteAllDescendants: settings?.noteAllDescendants,
@ -66,8 +71,8 @@ export default function Settings ({ data: { settings } }) {
greeterMode: settings?.greeterMode greeterMode: settings?.greeterMode
}} }}
schema={SettingsSchema} schema={SettingsSchema}
onSubmit={async ({ tipDefault, ...values }) => { onSubmit={async ({ tipDefault, fiatCurrency, ...values }) => {
await setSettings({ variables: { tipDefault: Number(tipDefault), ...values } }) await setSettings({ variables: { tipDefault: Number(tipDefault), fiatCurrency, ...values } })
setSuccess('settings saved') setSuccess('settings saved')
}} }}
> >
@ -79,6 +84,12 @@ export default function Settings ({ data: { settings } }) {
autoFocus autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
<Dropdown
label='fiat currency'
name='fiatCurrency'
items={supportedCurrencies}
required
/>
<div className='form-label'>notify me when ...</div> <div className='form-label'>notify me when ...</div>
<Checkbox <Checkbox
label='I stack sats from posts and comments' label='I stack sats from posts and comments'

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "fiatCurrency" TEXT NOT NULL DEFAULT 'USD';

View File

@ -36,6 +36,7 @@ model User {
freePosts Int @default(2) freePosts Int @default(2)
checkedNotesAt DateTime? checkedNotesAt DateTime?
tipDefault Int @default(10) tipDefault Int @default(10)
fiatCurrency String @default("USD")
pubkey String? @unique pubkey String? @unique
trust Float @default(0) trust Float @default(0)
upvoteTrust Float @default(0) upvoteTrust Float @default(0)