4. Adding, Editing and Deleting shareholders - PolymathNetwork/whitelist-standalone GitHub Wiki

Now that we have loaded and displayed the shareholders list, we can add more interesting features. We're going to enable token issuer to add, edit or remove shareholders.

We'll use one form component for adding new shareholders as well as editing existing ones. The only different is that address textfield is going to be disabled while editing a shareholder. That's because address is the primary key.

Components and Helper functions

First, we're going to add a couple of event handlers to App component. You can add these functions anywhere in App function, before returning:

// App.js
function App() {
  ...
  // Used for both adding a new shareholder and modifying an existing one.
  async function modifyWhitelist(data) {
    const queue = await tokens[selectedToken].shareholders.modifyData({
      shareholderData: data
    })
    await queue.run()
    dispatch({type:a.RELOAD_SHAREHOLDERS})
  }

  async function removeShareholders(addresses) {
    dispatch({type: a.DELETING_SHAREHOLDER})
    const queue = await tokens[selectedToken].shareholders.revokeKyc({
      shareholderAddresses: addresses
    })
    await queue.run()
    dispatch({type: a.SHAREHOLDER_DELETED})
    dispatch({type:a.RELOAD_SHAREHOLDERS})
  }
  ...
  return (
    ...
    { selectedToken !== undefined &&
      <Whitelist
        shareholders={shareholders}
        // Note these additional properties
        modifyWhitelist={modifyWhitelist}
        removeShareholders={removeShareholders}
        />
    }
    ...
  )
  • modifyWhitelist: A submit handler for shareholder form. The handler receives a Shareholder object, whether it's being edited or inserted. Eventually, the function uses SDK's modifyData as follows:
const queue = await tokens[selectedToken].shareholders.modifyData({
  // data is an array of shareholder objects.
  shareholderData: data
})
await queue.run()
  • removeShareholders which is the handler for the delete button. It will call SDK's revokeKyc to revoke KYC for passed addresses.

Note that we've previously dealt with similar async actions as an effect. We can still use the same setup here. For example. instead of calling removeShareholders directly from the delete handler, we could just the records to delete into app state, and then have an effect pick them up for deletion, on the next App component re-rendering. We've opted for calling removeShareholders directly because it's less verbose.

How that we've got our ducks in a row, we'll go ahead and add the add/edit shareholder form. But first we'll create a wrapper component Whitelist, which will wrap the form as well as the shareholders table we've created in previous steps.

// Whitelist.js
export default ({shareholders, modifyWhitelist, removeShareholders}) => {
  const form = useForm()
  // These functions are provided by rc-form-hooks library. See https://github.com/mushan0x0/rc-form-hooks
  const { getFieldDecorator, setFieldsValue, resetFields, validateFields } = form
  // Note this is a different reducer function from the one we created earlier. We're using this reducer particularly for form state.
  const [state, dispatch] = useReducer(reducer, initialState)
  const { visible, editIndex, ongoingTx } = state

  const closeForm = () => {
    dispatch({type: 'CLOSE_FORM'})
    resetFields()
  }
  
  // Passing an index means that we're editing an existing item. Otherwise it's a create form.
  const openForm = (index = '') => {
    dispatch({ type: 'OPEN_FORM', payload: { editIndex: index } })
  }

  const submitForm = async () => {
    const fields = ['address', 'canSendAfter', 'canReceiveAfter', 'kycExpiry', 'canBuyFromSto', 'isAccredited']
    validateFields(fields, { force: true })
      .then(async (values) => {
        // The values below are instances of momentjs. Convert them to JS Date objects as the SDK expects.
        values.canSendAfter = values.canSendAfter.toDate()
        values.canReceiveAfter = values.canReceiveAfter.toDate()
        values.kycExpiry = values.kycExpiry.toDate()

        try {
          dispatch({type: 'TX_SEND'})
          // Call the helper function, which will the SDK in its turn.
          await modifyWhitelist([values])
          dispatch({ type: 'TX_RECEIPT'})
          resetFields()
        }
        catch (error) {
          // This could be transaction error, or a user error e.g user rejected transaction.
          dispatch({ type: 'TX_ERROR',
            payload: {error: error.message} })
          message.error(error.message)
        }
      })
  }

  let editedRecord = shareholders.filter(shareholder => shareholder.address === editIndex)[0]
  // This effect sets form initial values. In "edit" mode, initial values reflect the currently edited record.
  useEffect(() => {
    let initialValues = editedRecord || defaultShareholderValues
    setFieldsValue(initialValues)
  }, [editedRecord, setFieldsValue])

  return (
    <div style={{display: 'flex',
      flexDirection: 'column'}}>
      <Button onClick={openForm}>Add new</Button>
      {/* This is the component from the previous guide */}
      <ShareholdersTable shareholders={shareholders} removeShareholders={removeShareholders} openForm={openForm} />
      <Modal
        title={editedRecord ? 'Edit token holder' : 'Add a new token holder'}
        closable={false}
        visible={visible}
        footer={null}
      >
        <Spin spinning={ongoingTx} size="large">
          <Form {...formItemLayout}>
            <Item name="address" label="Address">
              // here we add a couple of address validators
              // - The first make sure that the address is a valid ethereum address
              // - The second make sure we're not adding an existing shareholder.
              {getFieldDecorator('address', {
                rules: [
                  { required: true  }, {
                    validator: (rule, value, callback) => {
                      if (!editedRecord && !web3Utils.isAddress(value)) {
                        callback('Address is invalid') return
                      }
                      callback() return
                    }
                  }, {
                    validator: (rule, value, callback) => {
                      const shareholderExists = (address) => {
                        const ret =  shareholders.find((element) => element.address.toUpperCase() === address.toUpperCase())
                            !== undefined
                        return ret
                      }
                      if (!editedRecord && shareholderExists(value)) {
                        callback('Shareholder is already present in the whitelist') return
                      }
                      callback() return
                    }
                  }
                ],
                // Disable address field in case of editing.
              })(<Input disabled={!!editedRecord}/>)}
            </Item>
            <Item name="canSendAfter"  label="Can Send after">
              {getFieldDecorator('canSendAfter', {
                rules: [{ required: true }],
              })(<DatePicker />)}
            </Item>
            <Item name="canReceiveAfter" label="Can Receive After">
              {getFieldDecorator('canReceiveAfter', {
                rules: [{ required: true }],
              })(<DatePicker />)}
            </Item>
            <Item name="kycExpiry" label="KYC Expiry">
              {getFieldDecorator('kycExpiry', {
                rules: [{ required: true }],
              })(<DatePicker />)}
            </Item>
            <Item name="canBuyFromSto" label="Can Buy from STO">
              {getFieldDecorator('canBuyFromSto', {
                valuePropName: 'checked',
              })(<Switch />)}
            </Item>
            <Item name="isAccredited" label="Accredited">
              {getFieldDecorator('isAccredited', {
                valuePropName: 'checked',
              })(<Switch />)}
            </Item>
            <Item>
              <Button onClick={closeForm}>cancel</Button>
              <Button type="primary" onClick={submitForm}>save</Button>
            </Item>
          </Form>
        </Spin>
      </Modal>
    </div>
  )
}

Again, this is just a simple form wrapper by a component. It might look longer than needed, but that's only because field validation with Ant Design is a little awkward. Feel free to substitute for ant design with any other library you're familiar with.

Reducers

This is a brand-new reducer function. We opted to use a separate reducer for form and transactions states in order not to muddy up our existing one.

const reducer = (state, action) => {
  switch (action.type) {
  case 'OPEN_FORM':
    const { editIndex } = action.payload
    return {
      ...state,
      visible: true,
      editIndex
    }
  case 'CLOSE_FORM':
    return {
      ...state,
      editIndex: '',
      visible: false,
      ongoingTx: false
    }
  case 'TX_SEND':
    return {
      ...state,
      ongoingTx: true
    }
  case 'TX_RECEIPT':
    return {
      ...state,
      ongoingTx: false,
      visible: false,
      error: '',
      editIndex: ''
    }
  case 'TX_ERROR':
    const { error } = action.payload
    return {
      ...state,
      error,
      ongoingTx: false
    }
  default:
    return state
  }
}

The reducer above is responding to two kinds of actions:

  • Form actions like opening or closing a form. In those cases we manipulate a few flags that determine whether the modal is open and whether a spinner is visible.
  • Transaction state actions. These actions aim to display transaction status.
⚠️ **GitHub.com Fallback** ⚠️