diff --git a/web/cashtab-components/README.md b/web/cashtab-components/README.md index 8620f372a..f0a5690ae 100644 --- a/web/cashtab-components/README.md +++ b/web/cashtab-components/README.md @@ -1,206 +1,206 @@ # Cashtab React Components -Interact with the Cashtab wallet to support BCHA payments in web apps. Download the Cashtab extension [here](https://chrome.google.com/webstore/detail/cashtab/obldfcmebhllhjlhjbnghaipekcppeag) +Interact with the Cashtab wallet to support eCash payments in web apps. Download the Cashtab extension [here](https://chrome.google.com/webstore/detail/cashtab/obldfcmebhllhjlhjbnghaipekcppeag) This project is based on [badger-components-react](https://github.com/Bitcoin-com/badger-components-react) ## Notes Some features from `badger-components-react` are not yet fully supported. Upcoming features: - [ ] SLPA transactions - [ ] OP Return text - [ ] successFn and failureFn props - [ ] Bip70-style invoices - [ ] Websocket monitoring of payment status -# Build on Bitcoin ABC (BCHA) +# Build on eCash (XEC) -A set of React components and helpers to integrate Bitcoin ABC (BCHA) and tokens into your app with ease. Integrates with the Cashtab wallet. +A set of React components and helpers to integrate eCash (XEC) and tokens into your app with ease. Integrates with the Cashtab wallet. ## Get Started -- [Homepage](https://bitcoinabc.org) +- [Homepage](https://e.cash/) - [Component Showcase](https://laughing-villani-8cfcaf.netlify.app/) - [Cashtab Extension](https://chrome.google.com/webstore/detail/cashtab/obldfcmebhllhjlhjbnghaipekcppeag) - [NPM page](https://www.npmjs.com/package/cashtab-components) ### Install Component ```bash $ npm install --save cashtab-components ``` ### Install Peer Dependencies This library depends on the following three peer dependencies - `styled-components` ^4.4.1 ```bash $ npm install --save styled-components@4.4.1 ``` ### Add to React Project ```js import { CashtabButton, CashtabBadge } from 'cashtab-components'; const Example = props => { // eatBCH bitcoin cash address const toAddress = 'bitcoincash:pp8skudq3x5hzw8ew7vzsw8tn4k8wxsqsv0lt0mf3g'; // Random SLP address const toSLPAddress = 'simpleledger:qq6qcjt6xlkeqzdwkhdvfyl2q2d2wafkgg8phzcqez'; // tokenId const nakamotoID = 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb'; return ( <> {/* Minimal Examples */} {/* Price in bcha */} {/* Price in SLP tokens - NAKAMOTO in this example */} {/* More Complex Examples, pricing in fiat */} console.log('Payment success callback')} failFn={() => console.warn('Payment failed or cancelled callback') } /> console.log('success example function called')} failFn={() => console.log('fail example function called')} /> {/* Pricing in BCHA */} ); }; export default Example; ``` ### Create a Custom Cashtab Button / Integration ```js import React from 'react' import { CashtabBase, formatAmount } from 'cashtab-components' import styled from 'styled-components' const CoolButton = styled.button` background-color: rebeccapurple; color: lime; border-radius: 24px; ` const MyButton extends React.Component { render() { // Props from higher order component const { handleClick, to, step, price, currency, coinType, coinDecimals, coinSymbol, amount, showQR, isRepeatable, repeatTimeout, watchAddress, } = this.props; return (

Donate {price}{currency} to {to}

Satoshis: {formatAmount(amount, coinDecimals)}

Custom looking button with render
) } } // Wrap with CashtabBase higher order component export default CashtabBase(MyButton); ``` ### Control Step from app When accepting payments, the state of the payment should be handled by the backend of your application. As such, you can pass in `stepControlled` with the values of `fresh`, `pending` or `complete` to indicate which part of the payment the user is on. ## Development with Storybook To develop additions to this project, run the local storybook development server with ### Setup ```bash $ npm i $ npm run storybook ``` Navigate to [http://localhost:9001](http://localhost:9001) to view your stories. They automatically update as you develop ✨. Storybook will pick up stories from the `*stories.tsx` file in each components folder. To build a static version of storybook for deployment ```bash $ npm run build-storybook Deploy contents of `/storybook-static` ``` diff --git a/web/cashtab-components/package.json b/web/cashtab-components/package.json index daebd3078..fca0637db 100644 --- a/web/cashtab-components/package.json +++ b/web/cashtab-components/package.json @@ -1,123 +1,125 @@ { "name": "cashtab-components", - "version": "0.0.4", + "version": "1.0.0", "description": "Integrate the Cashtab Wallet into your React app with ease.", "main": "dist/index", "typings": "dist/index", "peerDependencies": { "react": "^17.0.1", "react-dom": "^17.0.1" }, "dependencies": { "@babel/preset-typescript": "^7.12.7", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/node": "^12.0.0", "@types/react": "^16.9.53", "@types/react-dom": "^16.9.8", "bignumber.js": "^9.0.1", "qrcode.react": "^1.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", "react-scripts": "4.0.1", "styled-components": "4.4.1", "typescript": "^4.0.3", "web-vitals": "^0.2.4", "webpack": "^4.44.2", "webpack-bundle-analyzer": "^4.3.0", "webpack-cli": "^4.3.0" }, "scripts": { "start": "npm run storybook", "build": "rm -rf dist/ && webpack --mode=production", "prepublish": "npm run build", "test": "jest", "eject": "react-scripts eject", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public" }, "repository": { "type": "git", "url": "git+https://github.com/josephroyking/cashtab-components.git" }, "keywords": [ "cashtab", "bitcoin abc", + "ecash", + "XEC", + "etoken", "web payments", "react component", - "BCHA", "cashtab button", "crypto payments", "crypto" ], "author": "Joey King ", "bugs": { "url": "https://github.com/josephroyking/cashtab-components/issues" }, "homepage": "https://github.com/josephroyking/cashtab-components#readme", "license": "MIT", "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/preset-env": "^7.12.11", "@storybook/addon-actions": "^6.1.11", "@storybook/addon-essentials": "^6.1.11", "@storybook/addon-knobs": "^6.1.11", "@storybook/addon-links": "^6.1.11", "@storybook/addons": "^6.1.11", "@storybook/node-logger": "^6.1.11", "@storybook/preset-create-react-app": "^3.1.5", "@storybook/react": "^6.1.11", "@storybook/theming": "^6.1.11", "@types/jest": "^26.0.19", "@types/lodash": "^4.14.165", "@types/qrcode.react": "^1.0.1", "@types/styled-components": "^5.1.7", "babel-jest": "^26.6.3", "jest": "^26.6.0", "jest-environment-jsdom-fifteen": "^1.0.2", "jest-junit": "^12.0.0", "ts-loader": "^8.0.12" }, "jest": { "roots": [ "/src" ], "setupFiles": [ "react-app-polyfill/jsdom" ], "testMatch": [ "/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "/src/**/*.{spec,test}.{js,jsx,ts,tsx}" ], "testEnvironment": "jest-environment-jsdom-fifteen", "transform": { "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest" }, "transformIgnorePatterns": [ "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$", "^.+\\.module\\.(css|sass|scss)$" ], "moduleNameMapper": { ".+\\.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$": "identity-obj-proxy" } } } diff --git a/web/cashtab-components/public/cashtab-components.png b/web/cashtab-components/public/cashtab-components.png index 25fe08269..e9f8023ac 100644 Binary files a/web/cashtab-components/public/cashtab-components.png and b/web/cashtab-components/public/cashtab-components.png differ diff --git a/web/cashtab-components/public/favicon.ico b/web/cashtab-components/public/favicon.ico index db48048f6..a4f74e528 100644 Binary files a/web/cashtab-components/public/favicon.ico and b/web/cashtab-components/public/favicon.ico differ diff --git a/web/cashtab-components/src/atoms/Ticker/Ticker.tsx b/web/cashtab-components/src/atoms/Ticker/Ticker.tsx index 0e2ae843e..48005593f 100644 --- a/web/cashtab-components/src/atoms/Ticker/Ticker.tsx +++ b/web/cashtab-components/src/atoms/Ticker/Ticker.tsx @@ -1,24 +1,24 @@ -import mainLogo from '../../images/12-bitcoin-cash-square-crop.svg'; -import tokenLogo from '../../images/simple-ledger-protocol-logo.png'; +import mainLogo from '../../images/logo_primary.png'; +import tokenLogo from '../../images/logo_secondary.png'; const Ticker = { installLink: 'https://chrome.google.com/webstore/detail/cashtab/obldfcmebhllhjlhjbnghaipekcppeag', - coinName: 'Bitcoin ABC', - coinDecimals: 8, - coinSymbol: 'BCHA', + coinName: 'eCash', + coinDecimals: 2, + coinSymbol: 'XEC', logo: mainLogo, - prefix: 'bitcoincash:', + prefix: 'ecash:', coingeckoId: 'bitcoin-cash-abc-2', defaultFee: 5.01, blockExplorerUrl: 'https://explorer.bitcoinabc.org', blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', - tokenName: 'Bitcoin ABC SLP', - tokenTicker: 'SLPA', + tokenName: 'eToken', + tokenTicker: 'eToken', tokenLogo: tokenLogo, - tokenPrefix: 'simpleledger:', + tokenPrefix: 'etoken:', tokenIconsUrl: '', //https://tokens.bitcoin.com/32 for BCH SLP useBlockchainWs: false, }; export default Ticker; diff --git a/web/cashtab-components/src/components/CashtabBadge/CashtabBadge.stories.tsx b/web/cashtab-components/src/components/CashtabBadge/CashtabBadge.stories.tsx index 6053afba2..a31985cc3 100644 --- a/web/cashtab-components/src/components/CashtabBadge/CashtabBadge.stories.tsx +++ b/web/cashtab-components/src/components/CashtabBadge/CashtabBadge.stories.tsx @@ -1,109 +1,109 @@ import { Meta, Story } from '@storybook/react/types-6-0'; import CashtabBadge from './CashtabBadge'; import type { CashtabBadgeProps } from './CashtabBadge'; import { currencyOptions } from '../../utils/currency-helpers'; import Ticker from '../../atoms/Ticker'; // [ SPICE, NAKAMOTO, DOGECASH, BROC ] const tokenIdOptions = [ '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', '3916a24a051f8b3833a7fd128be51dd93015555ed9142d6106ec03267f5cdc4c', '259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1', ]; const Template: Story = (args: CashtabBadgeProps) => ( ); export const Standard = Template.bind({}); Standard.args = { price: 0.05, currency: 'USD', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; export const MostProps = Template.bind({}); MostProps.args = { price: 0.0025, currency: 'GBP', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', isRepeatable: true, repeatTimeout: 4000, text: 'My Cash Button', showAmount: true, showBorder: true, showQR: false, }; export const Minimal = Template.bind({}); Minimal.args = { amount: 0.01, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', showAmount: false, showQR: true, }; export const Fiat = Template.bind({}); Fiat.args = { price: 3.5, currency: 'CAD', text: 'Pay with Cashtab', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; Fiat.storyName = 'price in fiat'; export const BCHA = Template.bind({}); BCHA.args = { coinType: Ticker.coinSymbol, amount: 0.33, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; BCHA.storyName = `price in ${Ticker.coinSymbol}`; export const SLPA = Template.bind({}); SLPA.args = { coinType: Ticker.tokenTicker, tokenId: tokenIdOptions[0], amount: 100, - to: 'simpleledger:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuqw7ew08n', + to: 'etoken:qrcl220pxeec78vnchwyh6fsdyf60uv9tcnqnc3hmv', showQR: true, }; SLPA.storyName = `price in ${Ticker.tokenTicker}`; export const StepControlled = Template.bind({}); StepControlled.args = { amount: 0.012, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', stepControlled: 'fresh', }; StepControlled.storyName = `Controlled Step`; export default { title: 'CashtabBadge', component: CashtabBadge, argTypes: { currency: { control: { type: 'select', options: currencyOptions, }, }, tokenId: { control: { type: 'select', options: tokenIdOptions, }, }, stepControlled: { control: { type: 'select', options: ['fresh', 'pending', 'complete'], }, }, }, } as Meta; diff --git a/web/cashtab-components/src/components/CashtabButton/CashtabButton.stories.tsx b/web/cashtab-components/src/components/CashtabButton/CashtabButton.stories.tsx index 11bc50534..4f4e53887 100644 --- a/web/cashtab-components/src/components/CashtabButton/CashtabButton.stories.tsx +++ b/web/cashtab-components/src/components/CashtabButton/CashtabButton.stories.tsx @@ -1,109 +1,109 @@ import { Meta, Story } from '@storybook/react/types-6-0'; import CashtabButton from './CashtabButton'; import type { CashtabButtonProps } from './CashtabButton'; import { currencyOptions } from '../../utils/currency-helpers'; import Ticker from '../../atoms/Ticker'; // [ SPICE, NAKAMOTO, DOGECASH, BROC ] const tokenIdOptions = [ '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf', 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb', '3916a24a051f8b3833a7fd128be51dd93015555ed9142d6106ec03267f5cdc4c', '259908ae44f46ef585edef4bcc1e50dc06e4c391ac4be929fae27235b8158cf1', ]; const Template: Story = (args: CashtabButtonProps) => ( ); export const Standard = Template.bind({}); Standard.args = { price: 0.05, currency: 'USD', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; export const MostProps = Template.bind({}); MostProps.args = { price: 0.0025, currency: 'GBP', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', isRepeatable: true, repeatTimeout: 4000, text: 'My Cash Button', showAmount: true, showBorder: true, showQR: false, }; export const Minimal = Template.bind({}); Minimal.args = { amount: 0.01, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', showAmount: false, showQR: true, }; export const Fiat = Template.bind({}); Fiat.args = { price: 3.5, currency: 'CAD', text: 'Pay with Cashtab', - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; Fiat.storyName = 'price in fiat'; export const BCHA = Template.bind({}); BCHA.args = { coinType: Ticker.coinSymbol, amount: 0.33, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', }; BCHA.storyName = `price in ${Ticker.coinSymbol}`; export const SLPA = Template.bind({}); SLPA.args = { coinType: Ticker.tokenTicker, tokenId: tokenIdOptions[0], amount: 100, - to: 'simpleledger:qqvcsnz9x9nu7vq35vmrkjc7hkfxhhs9nuqw7ew08n', + to: 'etoken:qrcl220pxeec78vnchwyh6fsdyf60uv9tcnqnc3hmv', showQR: true, }; SLPA.storyName = `price in ${Ticker.tokenTicker}`; export const StepControlled = Template.bind({}); StepControlled.args = { amount: 0.012, - to: 'bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev', + to: 'ecash:qrcl220pxeec78vnchwyh6fsdyf60uv9tca7668slm', stepControlled: 'fresh', }; StepControlled.storyName = `Controlled Step`; export default { title: 'CashtabButton', component: CashtabButton, argTypes: { currency: { control: { type: 'select', options: currencyOptions, }, }, tokenId: { control: { type: 'select', options: tokenIdOptions, }, }, stepControlled: { control: { type: 'select', options: ['fresh', 'pending', 'complete'], }, }, }, } as Meta; diff --git a/web/cashtab-components/src/components/PriceDisplay/PriceDisplay.stories.tsx b/web/cashtab-components/src/components/PriceDisplay/PriceDisplay.stories.tsx index 8a6429ef6..621998a38 100644 --- a/web/cashtab-components/src/components/PriceDisplay/PriceDisplay.stories.tsx +++ b/web/cashtab-components/src/components/PriceDisplay/PriceDisplay.stories.tsx @@ -1,75 +1,75 @@ import React from 'react'; import Ticker from '../../atoms/Ticker'; import { storiesOf } from '@storybook/react/dist/client/preview'; import { select, number } from '@storybook/addon-knobs'; import PriceDisplay from './PriceDisplay'; import { currencyOptions } from '../../utils/currency-helpers'; import { getCurrencyPreSymbol, formatPriceDisplay, formatAmount, } from '../../utils/cashtab-helpers'; storiesOf('Price Display', module) .addDecorator(story => (
{story()}
)) .add( 'fiat', () => { const currency = select('Currency', currencyOptions, 'USD'); const price = number('Price', 0.001); return ( ); }, { notes: 'Displaying fiat currencies', }, ) .add( - 'bch', + 'xec', () => { const price = number('Price', 0.001); const satoshis = price * 1e8; return ( ); }, { notes: `Displaying ${Ticker.coinSymbol}`, }, ) .add( `${Ticker.tokenTicker}`, () => { const price = number('Price', 0.001); const satoshis = price * 1e8; return ( ); }, { notes: `Displaying ${Ticker.tokenTicker} tokens`, }, ); diff --git a/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx b/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx index 9f4a6ad3f..3fa2ba58c 100644 --- a/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx +++ b/web/cashtab-components/src/hoc/CashtabBase/CashtabBase.tsx @@ -1,424 +1,430 @@ import * as React from 'react'; import debounce from 'lodash/debounce'; import { fiatToSatoshis, adjustAmount, getAddressUnconfirmed, getTokenInfo, } from '../../utils/cashtab-helpers'; import Ticker from '../../atoms/Ticker'; import type { CurrencyCode } from '../../utils/currency-helpers'; declare global { interface Window { bitcoinAbc: any; } } interface sendParamsArr { to: string; protocol: ValidCoinTypes; value?: string; assetId?: string; opReturn?: string[]; } const SECOND = 1000; const PRICE_UPDATE_INTERVAL = 60 * SECOND; const REPEAT_TIMEOUT = 4 * SECOND; const URI_CHECK_INTERVAL = 10 * SECOND; // Whitelist of valid coinType. type ValidCoinTypes = string; // TODO - Install is a Cashtab state, others are payment states. Separate them to be independent type ButtonStates = 'fresh' | 'pending' | 'complete' | 'expired' | 'install'; type CashtabBaseProps = { to: string; stepControlled?: ButtonStates; // Both present to price in fiat equivalent currency: CurrencyCode; price?: number; // Both present to price in coinType absolute amount coinType: ValidCoinTypes; tokenId?: string; amount?: number; isRepeatable: boolean; repeatTimeout: number; watchAddress: boolean; opReturn?: string[]; showQR: boolean; // Intent to show QR. Only show if amount is BCH or fiat as OP_RETURN and SLP do not work with QR successFn?: Function; failFn?: Function; }; interface IState { step: ButtonStates; errors: string[]; satoshis?: number; // Used when converting fiat to BCH coinSymbol?: string; coinName?: string; coinDecimals?: number; unconfirmedCount?: number; intervalPrice?: NodeJS.Timeout; intervalUnconfirmed?: NodeJS.Timeout; intervalTimer?: NodeJS.Timeout; } const CashtabBase = (Wrapped: React.ComponentType) => { return class extends React.Component { static defaultProps = { currency: 'USD', coinType: Ticker.coinSymbol, isRepeatable: false, watchAddress: false, showQR: true, repeatTimeout: REPEAT_TIMEOUT, }; state = { step: 'fresh' as ButtonStates, satoshis: undefined, coinSymbol: undefined, coinDecimals: undefined, coinName: undefined, unconfirmedCount: undefined, intervalPrice: undefined, intervalUnconfirmed: undefined, intervalTimer: undefined, errors: [], }; addError = (error: string) => { const { errors } = this.state; this.setState({ errors: [...errors, error] }); }; startRepeatable = () => { const { repeatTimeout } = this.props; setTimeout(() => this.setState({ step: 'fresh' }), repeatTimeout); }; paymentSendSuccess = () => { const { isRepeatable } = this.props; const { intervalUnconfirmed, unconfirmedCount } = this.state; let unconfirmedCountInt; if (typeof unconfirmedCount === 'undefined') { unconfirmedCountInt = 0; } else { unconfirmedCountInt = unconfirmedCount; } this.setState({ step: 'complete', unconfirmedCount: unconfirmedCountInt ? unconfirmedCountInt + 1 : 1, }); if (isRepeatable) { this.startRepeatable(); } else { intervalUnconfirmed && clearInterval(intervalUnconfirmed); } }; getCashTabProviderStatus = () => { console.log(window.bitcoinAbc); if ( window && window.bitcoinAbc && window.bitcoinAbc === 'cashtab' ) { return true; } return false; }; handleClick = () => { const { amount, to, opReturn, coinType, tokenId } = this.props; const { satoshis } = this.state; // Satoshis might not set be set during server rendering if (!amount && !satoshis) { return; } const walletProviderStatus = this.getCashTabProviderStatus(); if (typeof window === `undefined` || !walletProviderStatus) { this.setState({ step: 'install' }); if (typeof window !== 'undefined') { window.open(Ticker.installLink); } return; } if (walletProviderStatus) { this.setState({ step: 'fresh' }); return window.postMessage( { type: 'FROM_PAGE', text: 'CashTab', txInfo: { address: to, - value: satoshis ? satoshis! / 1e8 : amount, + value: satoshis + ? parseFloat( + ( + satoshis! * + 10 ** (-1 * Ticker.coinDecimals) + ).toFixed(2), + ) + : amount, }, }, '*', ); } const sendParams: sendParamsArr = { to, protocol: coinType, value: amount?.toString() || adjustAmount(satoshis, 8, true), }; if (coinType === Ticker.tokenTicker) { sendParams.assetId = tokenId; } if (opReturn && opReturn.length) { sendParams.opReturn = opReturn; } this.setState({ step: 'pending' }); /* May match this functionality later, may handle differently as above for Cashtab console.info('Cashtab sendAssets begin', sendParams); sendAssets(sendParams) .then(({ txid }: any) => { console.info('Cashtab send success:', txid); successFn && successFn(txid); this.paymentSendSuccess(); }) .catch((err: any) => { console.info('Cashtab send cancel', err); failFn && failFn(err); this.setState({ step: 'fresh' }); }); */ }; updateSatoshisFiat = debounce( async () => { const { price, currency } = this.props; if (!price) return; const satoshis = await fiatToSatoshis(currency, price); this.setState({ satoshis }); }, 250, { leading: true, trailing: true }, ); setupSatoshisFiat = () => { const { intervalPrice } = this.state; intervalPrice && clearInterval(intervalPrice); this.updateSatoshisFiat(); const intervalPriceNext = setInterval( () => this.updateSatoshisFiat(), PRICE_UPDATE_INTERVAL, ); this.setState({ intervalPrice: intervalPriceNext }); }; setupWatchAddress = async () => { const { to } = this.props; const { intervalUnconfirmed } = this.state; intervalUnconfirmed && clearInterval(intervalUnconfirmed); const initialUnconfirmed = await getAddressUnconfirmed(to); this.setState({ unconfirmedCount: initialUnconfirmed.length }); // Watch UTXO interval const intervalUnconfirmedNext = setInterval(async () => { const prevUnconfirmedCount = this.state.unconfirmedCount; const targetTransactions = await getAddressUnconfirmed(to); const unconfirmedCount = targetTransactions.length; this.setState({ unconfirmedCount }); if ( prevUnconfirmedCount != null && unconfirmedCount > prevUnconfirmedCount ) { this.paymentSendSuccess(); } }, URI_CHECK_INTERVAL); this.setState({ intervalUnconfirmed: intervalUnconfirmedNext }); }; setupCoinMeta = async () => { const { coinType, tokenId } = this.props; if (coinType === Ticker.coinSymbol) { this.setState({ coinSymbol: Ticker.coinSymbol, coinDecimals: Ticker.coinDecimals, coinName: Ticker.coinName, }); } else if (coinType === Ticker.tokenTicker && tokenId) { this.setState({ coinSymbol: undefined, coinName: undefined, coinDecimals: undefined, }); const tokenInfo = await getTokenInfo(tokenId); const { symbol, decimals, name } = tokenInfo; this.setState({ coinSymbol: symbol, coinDecimals: decimals, coinName: name, }); } }; confirmCashTabProviderStatus = () => { - console.log(`confirmCashTabProviderStatus called`); const cashTabStatus = this.getCashTabProviderStatus(); if (cashTabStatus) { this.setState({ step: 'fresh' }); } }; async componentDidMount() { if (typeof window !== 'undefined') { const { price, watchAddress } = this.props; // setup state, intervals, and listeners watchAddress && this.setupWatchAddress(); price && this.setupSatoshisFiat(); this.setupCoinMeta(); // normal call for setupCoinMeta() // Occasionially the cashtab window object is not available on componentDidMount, check later // TODO make this less hacky setTimeout(this.confirmCashTabProviderStatus, 750); // Detect CashTab and determine if button should show install CTA const walletProviderStatus = this.getCashTabProviderStatus(); if (walletProviderStatus) { this.setState({ step: 'fresh' }); } else { this.setState({ step: 'install' }); } } } componentWillUnmount() { const { intervalPrice, intervalUnconfirmed, intervalTimer, } = this.state; intervalPrice && clearInterval(intervalPrice); intervalUnconfirmed && clearInterval(intervalUnconfirmed); intervalTimer && clearInterval(intervalTimer); } componentDidUpdate(prevProps: CashtabBaseProps, prevState: IState) { if (typeof window !== 'undefined') { const { currency, price, isRepeatable, watchAddress, } = this.props; const prevCurrency = prevProps.currency; const prevPrice = prevProps.price; const prevIsRepeatable = prevProps.isRepeatable; const prevWatchAddress = prevProps.watchAddress; // Fiat price or currency changes if (currency !== prevCurrency || price !== prevPrice) { this.setupSatoshisFiat(); } if (isRepeatable && isRepeatable !== prevIsRepeatable) { this.startRepeatable(); } if (watchAddress !== prevWatchAddress) { if (watchAddress) { this.setupWatchAddress(); } else { const { intervalUnconfirmed } = this.state; intervalUnconfirmed && clearInterval(intervalUnconfirmed); } } } } render() { const { amount, showQR, opReturn, coinType, stepControlled, } = this.props; const { step, satoshis, coinDecimals, coinSymbol, coinName, } = this.state; let calculatedAmount = adjustAmount(amount, coinDecimals, false) || satoshis; // Only show QR if all requested features can be encoded in the BIP44 URI const shouldShowQR = showQR && coinType === Ticker.coinSymbol && (!opReturn || !opReturn.length); return ( ); } }; }; export type { CashtabBaseProps, ButtonStates, ValidCoinTypes, IState }; export default CashtabBase; diff --git a/web/cashtab-components/src/images/12-bitcoin-cash-square-crop.svg b/web/cashtab-components/src/images/12-bitcoin-cash-square-crop.svg deleted file mode 100644 index c3f7066cc..000000000 --- a/web/cashtab-components/src/images/12-bitcoin-cash-square-crop.svg +++ /dev/null @@ -1 +0,0 @@ -12-bitcoin-cash-square-crop \ No newline at end of file diff --git a/web/cashtab-components/src/images/bitcoin-cash-logo.svg b/web/cashtab-components/src/images/bitcoin-cash-logo.svg deleted file mode 100644 index ba5e07041..000000000 --- a/web/cashtab-components/src/images/bitcoin-cash-logo.svg +++ /dev/null @@ -1 +0,0 @@ -3-bitcoin-cash-logo-ot \ No newline at end of file diff --git a/web/cashtab-components/src/images/bitcoin-cash.svg b/web/cashtab-components/src/images/bitcoin-cash.svg deleted file mode 100644 index 2ac90462b..000000000 --- a/web/cashtab-components/src/images/bitcoin-cash.svg +++ /dev/null @@ -1 +0,0 @@ -4-bitcoin-cash-logo-flag \ No newline at end of file diff --git a/web/cashtab-components/src/images/logo_primary.png b/web/cashtab-components/src/images/logo_primary.png new file mode 100644 index 000000000..f739100b3 Binary files /dev/null and b/web/cashtab-components/src/images/logo_primary.png differ diff --git a/web/cashtab-components/src/images/logo_secondary.png b/web/cashtab-components/src/images/logo_secondary.png new file mode 100644 index 000000000..5223c3e1d Binary files /dev/null and b/web/cashtab-components/src/images/logo_secondary.png differ diff --git a/web/cashtab-components/src/images/simple-ledger-protocol-logo.png b/web/cashtab-components/src/images/simple-ledger-protocol-logo.png deleted file mode 100644 index 3e39423a8..000000000 Binary files a/web/cashtab-components/src/images/simple-ledger-protocol-logo.png and /dev/null differ diff --git a/web/cashtab-components/src/images/slp-logo.png b/web/cashtab-components/src/images/slp-logo.png deleted file mode 100644 index 76b7f13b7..000000000 Binary files a/web/cashtab-components/src/images/slp-logo.png and /dev/null differ diff --git a/web/cashtab-components/src/styles/colors.js b/web/cashtab-components/src/styles/colors.js index f355b349c..9d5d3f290 100644 --- a/web/cashtab-components/src/styles/colors.js +++ b/web/cashtab-components/src/styles/colors.js @@ -1,15 +1,15 @@ const colors = { - brand500: '#F59332', - brand700: '#F59332', + brand500: '#273498', + brand700: '#273498', fg100: '#ccc', fg500: '#4d4d4d', - pending500: '#00BFFF', - pending700: '#0070AA', - success500: '#00C571', - success700: '#007328', + pending500: '#FF21D0', + pending700: '#231F20', + success500: '#231F20', + success700: '#CD0BC3', expired500: '#FFBABA', expired700: '#D8000C', bg100: '#fff', }; export default colors; diff --git a/web/cashtab-components/src/utils/cashtab-helpers.ts b/web/cashtab-components/src/utils/cashtab-helpers.ts index 48b9d7f1a..ed4d90d6f 100644 --- a/web/cashtab-components/src/utils/cashtab-helpers.ts +++ b/web/cashtab-components/src/utils/cashtab-helpers.ts @@ -1,124 +1,129 @@ // Currency endpoints, logic, converters and formatters import BigNumber from 'bignumber.js'; import { currencySymbolMap } from './currency-helpers'; import type { CurrencyCode } from './currency-helpers'; import Ticker from '../atoms/Ticker/'; const buildPriceEndpoint = (currency: CurrencyCode) => { return `https://api.coingecko.com/api/v3/simple/price?ids=${Ticker.coingeckoId}&vs_currencies=${currency}&include_last_updated_at=true`; }; const getAddressUnconfirmed = async (address: string): Promise => { const transactionsRequest = await fetch( `https://rest.bitcoin.com/v2/address/unconfirmed/${address}`, ); const result = await transactionsRequest.json(); return result.utxos || []; }; const getTokenInfo = async (coinId: string): Promise => { const tokenInfoRequest = await fetch( `https://rest.bitcoin.com/v2/slp/list/${coinId}`, ); const tokenInfo = await tokenInfoRequest.json(); return tokenInfo; }; const getCurrencyPreSymbol = (currency: CurrencyCode) => { return currencySymbolMap[currency]; }; const formatPriceDisplay = (price?: number): string | undefined => { if (!price) return undefined; if (price > 1) { return price .toLocaleString('en-US', { style: 'currency', currency: 'USD', }) .slice(1); } else { return (+price).toFixed(5); } }; const formatAmount = (amount?: number, decimals?: number): string => { if (!decimals) { return '-.--------'; } if (!amount) { return `-.`.padEnd(decimals + 2, '-'); } const baseAmount = new BigNumber(amount); - const adjustDecimals = baseAmount - .shiftedBy(-1 * decimals) - .toFixed(decimals); - const removeTrailing = +adjustDecimals + ''; + const adjustDecimals = baseAmount.shiftedBy(-1 * decimals).toFixed(2); + const formatForLargeNum = parseFloat(adjustDecimals).toLocaleString( + undefined, + { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }, + ); + const removeTrailing = `${formatForLargeNum} `; return removeTrailing; }; const priceToSatoshis = (BCHRate: number, price: number): number => { const singleDollarValue = new BigNumber(BCHRate); const satoshisPerBCH = new BigNumber(100000000); const singleDollarSatoshis = satoshisPerBCH.div(singleDollarValue); return +singleDollarSatoshis .times(price) .integerValue(BigNumber.ROUND_FLOOR); }; const priceToFiat = (BCHRate: number, price: number): number => { const singleDollarValue = new BigNumber(BCHRate); return +singleDollarValue.times(price); }; const fiatToSatoshis = async ( currency: CurrencyCode, price: number, ): Promise => { const priceRequest = await fetch(buildPriceEndpoint(currency)); const result = await priceRequest.json(); const fiatPrice = result[Ticker.coingeckoId][currency.toLowerCase()]; const satoshis = priceToSatoshis(fiatPrice, price); return satoshis; }; const bchToFiat = async ( currency: CurrencyCode, price: number, ): Promise => { const priceRequest = await fetch(buildPriceEndpoint(currency)); const result = await priceRequest.json(); const fiatPrice = result[currency].rate; const fiatInvoiceTotal = priceToFiat(fiatPrice, price); return fiatInvoiceTotal; }; const adjustAmount = ( amount?: number, decimals?: number, fromSatoshis?: boolean, ): string | undefined => { decimals = decimals || 0; const shiftBy = !fromSatoshis ? decimals : decimals * -1; return amount ? new BigNumber(amount).shiftedBy(shiftBy).toString() : undefined; }; export { adjustAmount, buildPriceEndpoint, fiatToSatoshis, bchToFiat, formatAmount, formatPriceDisplay, getAddressUnconfirmed, getCurrencyPreSymbol, getTokenInfo, priceToSatoshis, };