Skip to content

Commit

Permalink
feat: add share button to receive screen (#310)
Browse files Browse the repository at this point in the history
  • Loading branch information
Daniel committed Jun 9, 2022
1 parent 8f62a42 commit ed03476
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 77 deletions.
6 changes: 6 additions & 0 deletions public/sprite.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 53 additions & 38 deletions src/components/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
import React, { PropsWithChildren, useState, useEffect, useRef } from 'react'
import { copyToClipboard } from '../utils'
import * as rb from 'react-bootstrap'
import Sprite from './Sprite'

const copyToClipboard = (
text: string,
fallbackInputField: HTMLInputElement,
errorMessage?: string
): Promise<boolean> => {
const copyToClipboardFallback = (
inputField: HTMLInputElement,
errorMessage = 'Cannot copy value to clipboard'
): Promise<boolean> =>
new Promise((resolve, reject) => {
inputField.select()
const success = document.execCommand && document.execCommand('copy')
inputField.blur()
success ? resolve(success) : reject(new Error(errorMessage))
})

// The `navigator.clipboard` API might not be available, e.g. on sites served over HTTP.
if (!navigator.clipboard) {
return copyToClipboardFallback(fallbackInputField)
}

return navigator.clipboard
.writeText(text)
.then(() => true)
.catch((e: Error) => {
if (fallbackInputField) {
return copyToClipboardFallback(fallbackInputField, errorMessage)
} else {
throw e
}
})
}

interface CopyableProps {
value: string
onSuccess?: () => void
onError?: (e: Error) => void
className?: string
}

function Copyable({ value, onError, onSuccess, className, children, ...props }: PropsWithChildren<CopyableProps>) {
function Copyable({ value, onSuccess, onError, className, children, ...props }: PropsWithChildren<CopyableProps>) {
const valueFallbackInputRef = useRef(null)

return (
<>
<button
<rb.Button
variant="outline-dark"
className={className}
{...props}
onClick={() => copyToClipboard(value, valueFallbackInputRef.current!).then(onSuccess, onError)}
>
{children}
</button>
</rb.Button>
<input
readOnly
aria-hidden
Expand All @@ -36,39 +70,21 @@ function Copyable({ value, onError, onSuccess, className, children, ...props }:
)
}

interface CopyButtonProps extends CopyableProps {}

export function CopyButton({ value, onSuccess, onError, children, ...props }: PropsWithChildren<CopyButtonProps>) {
return (
<Copyable
className="btn"
value={value}
onError={onError}
onSuccess={onSuccess}
data-bs-toggle="tooltip"
data-bs-placement="left"
{...props}
>
{children}
</Copyable>
)
}

interface CopyButtonWithConfirmationProps extends CopyButtonProps {
interface CopyButtonProps extends CopyableProps {
text: string
successText: string
successTextTimeout: number
}

export function CopyButtonWithConfirmation({
export function CopyButton({
value,
onSuccess,
onError,
text,
successText = text,
successTextTimeout = 1_500,
...props
}: CopyButtonWithConfirmationProps) {
className,
}: CopyButtonProps) {
const [showValueCopiedConfirmation, setShowValueCopiedConfirmation] = useState(false)
const [valueCopiedFlag, setValueCopiedFlag] = useState(0)

Expand All @@ -84,24 +100,23 @@ export function CopyButtonWithConfirmation({
}, [valueCopiedFlag, successTextTimeout])

return (
<CopyButton
className="btn btn-outline-dark"
<Copyable
className={className}
value={value}
onError={onError}
onSuccess={() => {
setValueCopiedFlag((current) => current + 1)
onSuccess && onSuccess()
}}
{...props}
>
{showValueCopiedConfirmation ? (
<div className="d-flex justify-content-center align-items-center">
{successText}
<Sprite color="green" symbol="checkmark" className="ms-1" width="20" height="20" />
</div>
) : (
<>{text}</>
)}
</CopyButton>
<div className="d-flex align-items-center justify-content-center">
{showValueCopiedConfirmation ? (
<Sprite color="green" symbol="checkmark" className="me-1" width="20" height="20" />
) : (
<Sprite symbol="copy" className="me-1" width="20" height="20" />
)}
{showValueCopiedConfirmation ? successText : text}
</div>
</Copyable>
)
}
17 changes: 12 additions & 5 deletions src/components/Receive.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { useCurrentWallet, useCurrentWalletInfo } from '../context/WalletContext
import * as Api from '../libs/JmWalletApi'
import PageTitle from './PageTitle'
import Sprite from './Sprite'
import { CopyButtonWithConfirmation } from './CopyButton'
import { CopyButton } from './CopyButton'
import { ShareButton, checkIsWebShareAPISupported } from './ShareButton'
import styles from './Receive.module.css'

export default function Receive() {
Expand Down Expand Up @@ -83,20 +84,26 @@ export default function Receive() {
</rb.Placeholder>
)}
</div>
<rb.Card.Body className={`${settings.theme === 'light' ? 'pt-0' : 'pt-3'} pb-0`}>
{address && <rb.Card.Text className="text-center slashed-zeroes">{address}</rb.Card.Text>}
<rb.Card.Body
className={`${settings.theme === 'light' ? 'pt-0' : 'pt-3'} pb-0 d-flex flex-column align-items-center`}
>
{address && (
<rb.Card.Text className={`${styles['address']} text-center slashed-zeroes`}>{address}</rb.Card.Text>
)}
{!address && (
<rb.Placeholder as="p" animation="wave" className={styles['receive-placeholder-container']}>
<rb.Placeholder xs={12} sm={10} md={8} className={styles['receive-placeholder']} />
</rb.Placeholder>
)}
<div className="d-flex justify-content-center" style={{ gap: '1rem' }}>
<CopyButtonWithConfirmation
<div className="d-flex justify-content-center gap-3 w-75">
<CopyButton
className="flex-1"
value={address}
text={t('receive.button_copy_address')}
successText={t('receive.text_copy_address_confirmed')}
disabled={!address || isLoading}
/>
{checkIsWebShareAPISupported() && <ShareButton value={address} className="flex-1" />}
</div>
</rb.Card.Body>
</rb.Card>
Expand Down
12 changes: 12 additions & 0 deletions src/components/Receive.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,15 @@
height: 16.25rem;
margin: 0;
}

.address {
font-size: 0.75rem;
padding: 1rem 0 1rem;
}

@media only screen and (min-width: 768px) {
.address {
font-size: var(--bs-body-font-size);
padding: 0;
}
}
35 changes: 35 additions & 0 deletions src/components/ShareButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import * as rb from 'react-bootstrap'
import Sprite from './Sprite'

const checkIsWebShareAPISupported = () => {
return !!navigator.share
}

const ShareButton = ({ value, className }) => {
const handleShare = async () => {
if (!checkIsWebShareAPISupported()) {
console.error('Sharing failed: Web Share API not supported.')
return
}

try {
await navigator.share({
text: value,
})
} catch (error) {
console.error(`Sharing failed: ${error}`)
}
}

return (
<rb.Button variant="outline-dark" className={className} onClick={handleShare}>
<div className="d-flex align-items-center justify-content-center">
<Sprite symbol="share" className="me-1" width="20" height="20" />
Share
</div>
</rb.Button>
)
}

export { ShareButton, checkIsWebShareAPISupported }
34 changes: 0 additions & 34 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,6 @@ export const btcToSats = (value: string) => Math.round(parseFloat(value) * 10000

export const satsToBtc = (value: string) => parseInt(value, 10) / 100000000

export const copyToClipboard = (
text: string,
fallbackInputField?: HTMLInputElement,
errorMessage?: string
): Promise<boolean> => {
const copyToClipboardFallback = (
inputField: HTMLInputElement,
errorMessage = 'Cannot copy value to clipboard'
): Promise<boolean> =>
new Promise((resolve, reject) => {
inputField.select()
const success = document.execCommand && document.execCommand('copy')
inputField.blur()
success ? resolve(success) : reject(new Error(errorMessage))
})

// `navigator.clipboard` might not be available, e.g. on sites served over plain `http`.
if (!navigator.clipboard && fallbackInputField) {
return copyToClipboardFallback(fallbackInputField)
}

// might not work on iOS.
return navigator.clipboard
.writeText(text)
.then(() => true)
.catch((e: Error) => {
if (fallbackInputField) {
return copyToClipboardFallback(fallbackInputField, errorMessage)
} else {
throw e
}
})
}

export const formatBtc = (value: number) => {
const decimalPoint = '\u002E'
const nbHalfSpace = '\u202F'
Expand Down

0 comments on commit ed03476

Please sign in to comment.