123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import BN from 'bn.js';
- import React from 'react';
- import { Link } from 'react-router-dom';
- import { Form, Field, withFormik, FormikProps } from 'formik';
- import * as Yup from 'yup';
- import { Option, Vec } from '@polkadot/types';
- import Section from '@polkadot/joy-utils/Section';
- import TxButton from '@polkadot/joy-utils/TxButton';
- import * as JoyForms from '@polkadot/joy-utils/forms';
- import { SubmittableResult } from '@polkadot/api';
- import { MemberId, UserInfo, Profile, PaidTermId, PaidMembershipTerms } from '@joystream/types/members';
- import { OptionText } from '@joystream/types/common';
- import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
- import { queryMembershipToProp } from './utils';
- import { withCalls } from '@polkadot/react-api/index';
- import { Button, Message } from 'semantic-ui-react';
- import { formatBalance } from '@polkadot/util';
- import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
- import isEqual from 'lodash/isEqual';
- // TODO get next settings from Substrate:
- const HANDLE_REGEX = /^[a-z0-9_]+$/;
- const buildSchema = (p: ValidationProps) =>
- Yup.object().shape({
- handle: Yup.string()
- .matches(HANDLE_REGEX, 'Handle can have only lowercase letters (a-z), numbers (0-9) and underscores (_).')
- .min(p.minHandleLength, `Handle is too short. Minimum length is ${p.minHandleLength} chars.`)
- .max(p.maxHandleLength, `Handle is too long. Maximum length is ${p.maxHandleLength} chars.`)
- .required('Handle is required'),
- avatar: Yup.string()
- .url('Avatar must be a valid URL of an image.')
- .max(p.maxAvatarUriLength, `Avatar URL is too long. Maximum length is ${p.maxAvatarUriLength} chars.`),
- about: Yup.string().max(p.maxAboutTextLength, `Text is too long. Maximum length is ${p.maxAboutTextLength} chars.`)
- });
- type ValidationProps = {
- minHandleLength: number;
- maxHandleLength: number;
- maxAvatarUriLength: number;
- maxAboutTextLength: number;
- };
- type OuterProps = ValidationProps & {
- profile?: Profile;
- paidTerms: PaidMembershipTerms;
- paidTermId: PaidTermId;
- memberId?: MemberId;
- };
- type FormValues = {
- handle: string;
- avatar: string;
- about: string;
- };
- type FieldName = keyof FormValues;
- type FormProps = OuterProps & FormikProps<FormValues>;
- const LabelledField = JoyForms.LabelledField<FormValues>();
- const LabelledText = JoyForms.LabelledText<FormValues>();
- const InnerForm = (props: FormProps) => {
- const {
- profile,
- paidTerms,
- paidTermId,
- initialValues,
- values,
- touched,
- dirty,
- isValid,
- isSubmitting,
- setSubmitting,
- resetForm,
- memberId
- } = props;
- const onSubmit = (sendTx: () => void) => {
- if (isValid) sendTx();
- };
- const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
- setSubmitting(false);
- if (txResult == null) {
- // Tx cancelled.
- }
- };
- const onTxSuccess: TxCallback = (_txResult: SubmittableResult) => {
- setSubmitting(false);
- };
- // TODO extract to forms.tsx
- const isFieldChanged = (field: FieldName): boolean => {
- return dirty && touched[field] === true && !isEqual(values[field], initialValues[field]);
- };
- // TODO extract to forms.tsx
- const fieldToTextOption = (field: FieldName): OptionText => {
- return isFieldChanged(field) ? OptionText.some(values[field]) : OptionText.none();
- };
- const buildTxParams = () => {
- if (!isValid) return [];
- const userInfo = new UserInfo({
- handle: fieldToTextOption('handle'),
- avatar_uri: fieldToTextOption('avatar'),
- about: fieldToTextOption('about')
- });
- if (profile) {
- // update profile
- return [memberId, userInfo];
- } else {
- // register as new member
- return [paidTermId, userInfo];
- }
- };
- // TODO show warning that you don't have enough balance to buy a membership
- return (
- <Section title="My Membership Profile">
- <Form className="ui form JoyForm">
- <LabelledText
- name="handle"
- label="Handle/nickname"
- placeholder={'You can use a-z, 0-9 and underscores.'}
- style={{ maxWidth: '30rem' }}
- {...props}
- />
- <LabelledText
- name="avatar"
- label="Avatar URL"
- placeholder="Paste here an URL of your avatar image."
- {...props}
- />
- <LabelledField name="about" label="About" {...props}>
- <Field
- component="textarea"
- id="about"
- name="about"
- disabled={isSubmitting}
- rows={3}
- placeholder="Write here anything you would like to share about yourself with Joystream community."
- />
- </LabelledField>
- {!profile && paidTerms && (
- <Message warning style={{ display: 'block', marginBottom: '0' }}>
- <p>
- Membership costs <b>{formatBalance(paidTerms.fee)}</b> tokens.
- </p>
- <p>
- <span>{'By clicking the "Register" button you agree to our '}</span>
- <Link to={'/pages/tos'}>Terms of Service</Link>
- <span> and </span>
- <Link to={'/pages/privacy'}>Privacy Policy</Link>.
- </p>
- </Message>
- )}
- <LabelledField invisibleLabel {...props}>
- <TxButton
- type="submit"
- size="large"
- label={profile ? 'Update my profile' : 'Register'}
- isDisabled={!dirty || isSubmitting}
- params={buildTxParams()}
- tx={profile ? 'members.updateProfile' : 'members.buyMembership'}
- onClick={onSubmit}
- txFailedCb={onTxFailed}
- txSuccessCb={onTxSuccess}
- />
- <Button
- type="button"
- size="large"
- disabled={!dirty || isSubmitting}
- onClick={() => resetForm()}
- content="Reset form"
- />
- </LabelledField>
- </Form>
- </Section>
- );
- };
- const EditForm = withFormik<OuterProps, FormValues>({
- // Transform outer props into form values
- mapPropsToValues: props => {
- const { profile: p } = props;
- return {
- handle: p ? p.handle.toString() : '',
- avatar: p ? p.avatar_uri.toString() : '',
- about: p ? p.about.toString() : ''
- };
- },
- validationSchema: buildSchema,
- handleSubmit: values => {
- // do submitting things
- }
- })(InnerForm);
- type WithMyProfileProps = {
- memberId?: MemberId;
- memberProfile?: Option<any>; // TODO refactor to Option<Profile>
- paidTermsId: PaidTermId;
- paidTerms?: Option<PaidMembershipTerms>;
- minHandleLength?: BN;
- maxHandleLength?: BN;
- maxAvatarUriLength?: BN;
- maxAboutTextLength?: BN;
- };
- function WithMyProfileInner (p: WithMyProfileProps) {
- const triedToFindProfile = !p.memberId || p.memberProfile;
- if (
- triedToFindProfile &&
- p.paidTerms &&
- p.minHandleLength &&
- p.maxHandleLength &&
- p.maxAvatarUriLength &&
- p.maxAboutTextLength
- ) {
- const profile = p.memberProfile ? p.memberProfile.unwrapOr(undefined) : undefined;
- if (!profile && p.paidTerms.isNone) {
- console.error('Could not find active paid membership terms');
- }
- return (
- <EditForm
- minHandleLength={p.minHandleLength.toNumber()}
- maxHandleLength={p.maxHandleLength.toNumber()}
- maxAvatarUriLength={p.maxAvatarUriLength.toNumber()}
- maxAboutTextLength={p.maxAboutTextLength.toNumber()}
- profile={profile as Profile}
- paidTerms={p.paidTerms.unwrap()}
- paidTermId={p.paidTermsId}
- memberId={p.memberId}
- />
- );
- } else return <em>Loading...</em>;
- }
- const WithMyProfile = withCalls<WithMyProfileProps>(
- queryMembershipToProp('minHandleLength'),
- queryMembershipToProp('maxHandleLength'),
- queryMembershipToProp('maxAvatarUriLength'),
- queryMembershipToProp('maxAboutTextLength'),
- queryMembershipToProp('memberProfile', 'memberId'),
- queryMembershipToProp('paidMembershipTermsById', { paramName: 'paidTermsId', propName: 'paidTerms' })
- )(WithMyProfileInner);
- type WithMyMemberIdProps = MyAccountProps & {
- memberIdsByRootAccountId?: Vec<MemberId>;
- memberIdsByControllerAccountId?: Vec<MemberId>;
- paidTermsIds?: Vec<PaidTermId>;
- };
- function WithMyMemberIdInner (p: WithMyMemberIdProps) {
- if (p.allAccounts && !Object.keys(p.allAccounts).length) {
- return (
- <Message warning className="JoyMainStatus">
- <Message.Header>Please create a key to get started.</Message.Header>
- <div style={{ marginTop: '1rem' }}>
- <Link to={'/accounts'} className="ui button orange">
- Create key
- </Link>
- </div>
- </Message>
- );
- }
- if (p.memberIdsByRootAccountId && p.memberIdsByControllerAccountId && p.paidTermsIds) {
- if (p.paidTermsIds.length) {
- // let member_ids = p.memberIdsByRootAccountId.slice(); // u8a.subarray is not a function!!
- p.memberIdsByRootAccountId.concat(p.memberIdsByControllerAccountId);
- const memberId = p.memberIdsByRootAccountId.length ? p.memberIdsByRootAccountId[0] : undefined;
- return <WithMyProfile memberId={memberId} paidTermsId={p.paidTermsIds[0]} />;
- } else {
- console.error('Active paid membership terms is empty');
- }
- }
- return <em>Loading...</em>;
- }
- const WithMyMemberId = withMyAccount(
- withCalls<WithMyMemberIdProps>(
- queryMembershipToProp('memberIdsByRootAccountId', 'myAddress'),
- queryMembershipToProp('memberIdsByControllerAccountId', 'myAddress'),
- queryMembershipToProp('activePaidMembershipTerms', { propName: 'paidTermsIds' })
- )(WithMyMemberIdInner)
- );
- export default WithMyMemberId;
|