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

Withdraw Options #597

Merged
merged 13 commits into from
Nov 11, 2021
3 changes: 3 additions & 0 deletions packages/webapp/.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ NEXT_PUBLIC_TABLE_ROWS_MAX_FETCH_PER_SEC = "10"
NEXT_PUBLIC_EDEN_CONTRACT_ACCOUNT = "test.edev"
NEXT_PUBLIC_AA_FETCH_AFTER="1633883520000"
NEXT_PUBLIC_TOKEN_CONTRACT = "eosio.token"
NEXT_PUBLIC_TOKEN_SYMBOL = "WAX"
NEXT_PUBLIC_TOKEN_PRECISION = "8"
brandonfancher marked this conversation as resolved.
Show resolved Hide resolved

# ATOMICHUB
NEXT_PUBLIC_AA_BASE_URL = "https://test.wax.api.atomicassets.io/atomicassets/v1"
Expand All @@ -27,6 +29,7 @@ NEXT_PUBLIC_AA_SCHEMA_NAME = "members"

# OTHER
NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL = "https://wax-test.bloks.io/account"
NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL = "https://wax-test.bloks.io/transaction"
NEXT_PUBLIC_APP_MINIMUM_DONATION_AMOUNT = "10.00000000 WAX"
NEXT_PUBLIC_ENABLED_WALLETS = "ANCHOR,LEDGER,SOFTKEY"
NEXT_PUBLIC_IPFS_BASE_URL = "https://infura-ipfs.io/ipfs"
Expand Down
12 changes: 0 additions & 12 deletions packages/webapp/src/_app/hooks/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from "inductions/api";
import {
getChiefDelegates,
getDistributionsForAccount,
getDistributionState,
getMasterPool,
getHeadDelegate,
Expand Down Expand Up @@ -200,11 +199,6 @@ export const queryMasterPool = () => ({
queryFn: getMasterPool,
});

export const queryDistributionsForAccount = (account: string) => ({
queryKey: ["query_distributions_for_account", account],
queryFn: () => getDistributionsForAccount(account),
});

export const queryTokenBalanceForAccount = (account: string) => ({
queryKey: ["query_token_balance_for_account", account],
queryFn: () => getTokenBalanceForAccount(account),
Expand Down Expand Up @@ -248,12 +242,6 @@ export const useMemberByAccountName = (accountName?: string) =>
enabled: Boolean(accountName),
});

export const useDistributionsForAccount = (account: string) =>
useQuery({
...queryDistributionsForAccount(account),
enabled: Boolean(account),
});

export const useDistributionState = () =>
useQuery({
...queryDistributionState(),
Expand Down
11 changes: 11 additions & 0 deletions packages/webapp/src/_app/styles/inputs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
134 changes: 130 additions & 4 deletions packages/webapp/src/_app/ui/form.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import React, { HTMLProps } from "react";

import { tokenConfig } from "config";
import { Button } from "_app";

export const Label: React.FC<{
htmlFor: string;
}> = (props) => (
<label className="block text-sm font-medium text-gray-700" {...props}>
<label className="block text-sm font-normal text-gray-600" {...props}>
{props.children}
</label>
);

export const Input: React.FC<HTMLProps<HTMLInputElement>> = (props) => (
export const Input: React.FC<
HTMLProps<HTMLInputElement> & {
inputRef?: React.Ref<HTMLInputElement> | null;
}
> = ({ inputRef, ...props }) => (
<input
name={props.id}
className={`w-full bg-white rounded border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out ${
className={`w-full bg-white border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 text-base outline-none text-gray-700 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out ${
props.disabled ? "bg-gray-50" : ""
}`}
ref={inputRef}
{...props}
/>
);
Expand Down Expand Up @@ -47,7 +55,7 @@ export const TextArea: React.FC<HTMLProps<HTMLTextAreaElement>> = (props) => (
<textarea
rows={3}
name={props.id}
className={`w-full bg-white rounded border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out ${
className={`w-full bg-white border border-gray-300 focus:border-yellow-500 focus:ring-2 focus:ring-yellow-200 h-32 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out ${
props.disabled ? "bg-gray-50" : ""
}`}
{...props}
Expand Down Expand Up @@ -99,6 +107,122 @@ export const LabeledSet: React.FC<{
);
};

export const ChainAccountInput = (
props: HTMLProps<HTMLInputElement> & { id: string }
) => {
const validateAccountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.valueMissing) {
target.setCustomValidity("Enter an account name");
} else {
target.setCustomValidity("Invalid account name");
}
};

const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).setCustomValidity("");
};

const onInput = (e: React.FormEvent<HTMLInputElement>) => {
clearErrorMessages(e);
props.onInput?.(e);
};

const onInvalid = (e: React.FormEvent<HTMLInputElement>) => {
validateAccountField(e);
props.onInvalid?.(e);
};

return (
<Form.LabeledSet
label={`${tokenConfig.symbol} account name (12 characters)`}
htmlFor={props.id}
>
<Form.Input
type="text"
maxLength={12}
pattern="^[a-z,1-5.]{1,12}$"
{...props}
onInvalid={onInvalid}
onInput={onInput}
/>
</Form.LabeledSet>
);
};

export const AssetInput = (
props: HTMLProps<HTMLInputElement> & {
label: string; // required
id: string; // required
inputRef?: React.RefObject<HTMLInputElement>;
}
) => {
const { label, inputRef, ...inputProps } = props;

const amountRef = inputRef ?? React.useRef<HTMLInputElement>(null);

const amountInputPreventChangeOnScroll = (
e: React.WheelEvent<HTMLInputElement>
) => (e.target as HTMLInputElement).blur();

const validateAmountField = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
if (target.validity.rangeOverflow) {
target.setCustomValidity("Insufficient funds available");
} else {
target.setCustomValidity("Enter a valid amount");
}
};

const clearErrorMessages = (e: React.FormEvent<HTMLInputElement>) => {
(e.target as HTMLInputElement).setCustomValidity("");
};

const setMaxAmount = () => {
amountRef.current?.setCustomValidity("");

// ensures this works with uncontrolled instances of this input too
Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set?.call?.(amountRef.current, inputProps.max);
amountRef.current?.dispatchEvent(
new Event("change", { bubbles: true })
);
// (adapted from: https://coryrylan.com/blog/trigger-input-updates-with-react-controlled-inputs)
};

return (
<Form.LabeledSet label={label} htmlFor={inputProps.id}>
<div className="flex space-x-2">
<div className="relative flex-1">
<Form.Input
type="number"
inputMode="decimal"
min={1 / Math.pow(10, tokenConfig.precision)}
step="any"
onWheel={amountInputPreventChangeOnScroll}
onInvalid={validateAmountField}
onInput={clearErrorMessages}
inputRef={amountRef}
{...inputProps}
/>
<div className="absolute top-3 right-2">
<p className="text-sm text-gray-400">
{tokenConfig.symbol}
</p>
</div>
</div>
{inputProps.max ? (
<Button type="neutral" onClick={setMaxAmount}>
Max
</Button>
) : null}
</div>
</Form.LabeledSet>
);
};

export const Form = {
Label,
Input,
Expand All @@ -107,6 +231,8 @@ export const Form = {
LabeledSet,
FileInput,
Checkbox,
ChainAccountInput,
AssetInput,
};

export default Form;
19 changes: 19 additions & 0 deletions packages/webapp/src/_app/utils/asset.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { tokenConfig } from "config";

export interface Asset {
quantity: number; // integer without decimals
symbol: string;
precision: number;
}

export const getDefaultTokenAsset = () => ({
quantity: 0,
symbol: tokenConfig.symbol,
precision: tokenConfig.precision,
});

export const assetFromString = (asset: string): Asset => {
const re = /^[0-9]+\.[0-9]+ [A-Z,a-z]{1,5}$/;
if (asset.match(re) === null) {
Expand All @@ -20,6 +28,17 @@ export const assetFromString = (asset: string): Asset => {
};
};

/**
* Convert a number to an Asset with the community-configured token symbol and precision
* @param {number} value - a number that can contain a decimal
* @returns {Asset} the Asset with the community's default token symbol and precision
*/
export const assetFromNumber = (value: number): Asset => ({
symbol: tokenConfig.symbol,
precision: tokenConfig.precision,
quantity: value * Math.pow(10, tokenConfig.precision),
});

export const assetToString = (price: Asset, decimals = 2) =>
`${(price.quantity / Math.pow(10, price.precision)).toFixed(decimals)} ${
price.symbol
Expand Down
16 changes: 16 additions & 0 deletions packages/webapp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ if (
!process.env.NEXT_PUBLIC_EOS_READ_RPC_URLS ||
!process.env.NEXT_PUBLIC_EOS_CHAIN_ID ||
!process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL ||
!process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL ||
!process.env.NEXT_PUBLIC_AA_BASE_URL ||
!process.env.NEXT_PUBLIC_AA_MARKET_URL ||
!process.env.NEXT_PUBLIC_AA_HUB_URL ||
Expand All @@ -28,6 +29,8 @@ if (
!process.env.NEXT_PUBLIC_ELECTION_COMMUNITY_ROOM_URL ||
!process.env.NEXT_PUBLIC_ELECTION_MEETING_DURATION_MS ||
!process.env.NEXT_PUBLIC_TOKEN_CONTRACT ||
!process.env.NEXT_PUBLIC_TOKEN_SYMBOL ||
!process.env.NEXT_PUBLIC_TOKEN_PRECISION ||
!process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL ||
!process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL ||
!process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL ||
Expand All @@ -48,6 +51,9 @@ EOS_CHAIN_ID="${process.env.NEXT_PUBLIC_EOS_CHAIN_ID}"
BLOCKEXPLORER_ACCOUNT_BASE_URL="${
process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL
}"
BLOCKEXPLORER_TRANSACTION_BASE_URL="${
process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL
}"
AA_BASE_URL="${process.env.NEXT_PUBLIC_AA_BASE_URL}"
AA_MARKET_URL="${process.env.NEXT_PUBLIC_AA_MARKET_URL}"
AA_HUB_URL="${process.env.NEXT_PUBLIC_AA_HUB_URL}"
Expand All @@ -74,6 +80,8 @@ ELECTION_COMMUNITY_ROOM_URL="${
process.env.NEXT_PUBLIC_ELECTION_COMMUNITY_ROOM_URL
}"
TOKEN_CONTRACT="${process.env.NEXT_PUBLIC_TOKEN_CONTRACT}"
TOKEN_SYMBOL="${process.env.NEXT_PUBLIC_TOKEN_SYMBOL}"
TOKEN_PRECISION="${process.env.NEXT_PUBLIC_TOKEN_PRECISION}"
SUBCHAIN_WASM_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_WASM_URL}"
SUBCHAIN_STATE_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_STATE_URL}"
SUBCHAIN_WS_URL="${process.env.NEXT_PUBLIC_SUBCHAIN_WS_URL}"
Expand Down Expand Up @@ -102,6 +110,8 @@ export const box = {

export const blockExplorerAccountBaseUrl =
process.env.NEXT_PUBLIC_BLOCKEXPLORER_ACCOUNT_BASE_URL;
export const blockExplorerTransactionBaseUrl =
process.env.NEXT_PUBLIC_BLOCKEXPLORER_TRANSACTION_BASE_URL;

export const shortAppName = process.env.NEXT_PUBLIC_APP_SHORT_NAME;
export const appName = process.env.NEXT_PUBLIC_APP_NAME;
Expand Down Expand Up @@ -137,6 +147,12 @@ export const chainConfig = {
rpcEndpoints: [rpcEndpoint],
};

export const tokenConfig = {
contract: process.env.NEXT_PUBLIC_TOKEN_CONTRACT,
symbol: process.env.NEXT_PUBLIC_TOKEN_SYMBOL,
precision: Number(process.env.NEXT_PUBLIC_TOKEN_PRECISION),
};

export const availableWallets = (
process.env.NEXT_PUBLIC_ENABLED_WALLETS || ""
).split(",");
Expand Down
Loading