Skip to content

Commit

Permalink
Support SEP-8 "Action Required" flow (#164)
Browse files Browse the repository at this point in the history
### What

Support SEP-8 "Action Required" flow.

Closes #113 

### Future Work

The action fields received in the action required responses are all being handled as strings for now. I'll update this behavior to accept different kinds of inputs in the future, since the [SEP-9] field types can include "string", "country code", "phone number", date", "number" or "binary".

[SEP-9]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0009.md

### Screenshot

https://user-images.githubusercontent.com/1952597/119165154-958bd600-ba33-11eb-9639-3d014c8003d4.mov

### How to test

Use the jenkins preview link with `/account?secretKey=SBQZPYFQNQD4B7MP7PC3NKCMS5G5LD6PVO7BUYC34M4BBFN6KCXSKSVD` and send a small payment to `GBID36ML6VVNPIF6SQATQW5QIBREQAVEHQIUMWDLQCSVIYX2IJGK4KPP`.

#### KYC Approval Criteria

* Emails starting with "x" will get rejected
* Emails not starting with "x" will get approved.

If you need to repeat your tests you might need to delete the KYC already stored in the server with:
```curl
curl -X DELETE https://sep8-server.dev.stellar.org/kyc-status/GASA5HCKMDAPLB6NIV7ADDA6YMTAQ2555TXKMS7FSVO44TJXHEGXLICZ
```
  • Loading branch information
marcelosalloum committed May 24, 2021
1 parent 0997f7c commit 619dbcc
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 59 deletions.
132 changes: 132 additions & 0 deletions src/components/Sep8Send/Sep8ActionRequiredForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { Button, Input, Loader } from "@stellar/design-system";
import { Heading2 } from "components/Heading";
import { Modal } from "components/Modal";
import {
initiateSep8SendAction,
sep8SendActionRequiredFieldsAction,
} from "ducks/sep8Send";
import { useRedux } from "hooks/useRedux";
import {
ActionStatus,
Sep8ActionRequiredResultType,
Sep8Step,
} from "types/types.d";

export const Sep8ActionRequiredForm = ({
onClose,
}: {
onClose: () => void;
}) => {
const { account, sep8Send } = useRedux("account", "sep8Send");
const [fieldValues, setFieldValues] = useState<{ [key: string]: string }>({});
const {
actionFields,
message,
actionMethod,
actionUrl,
} = sep8Send.data.actionRequiredInfo;
const { nextUrl, result } = sep8Send.data.actionRequiredResult;
const dispatch = useDispatch();

useEffect(() => {
if (sep8Send.data.sep8Step === Sep8Step.SENT_ACTION_REQUIRED_FIELDS) {
if (result === Sep8ActionRequiredResultType.NO_FURTHER_ACTION_REQUIRED) {
window.open(nextUrl, "_blank");
}

if (account.data) {
dispatch(
initiateSep8SendAction({
assetCode: sep8Send.data.assetCode,
assetIssuer: sep8Send.data.assetIssuer,
homeDomain: sep8Send.data.homeDomain,
}),
);
}
}
}, [
account.data,
dispatch,
nextUrl,
result,
sep8Send.data.assetCode,
sep8Send.data.assetIssuer,
sep8Send.data.homeDomain,
sep8Send.data.sep8Step,
]);

const handleSubmitActionRequiredFields = () => {
dispatch(
sep8SendActionRequiredFieldsAction({
actionFields: fieldValues,
actionMethod,
actionUrl,
}),
);
};

const handleOnChangeField = ({
fieldName,
fieldValue,
}: {
fieldName: string;
fieldValue: string;
}) => {
const buffFieldValue = { ...fieldValues };
buffFieldValue[fieldName] = fieldValue;
setFieldValues(buffFieldValue);
};

const renderSendPayment = () => (
<>
<Heading2 className="ModalHeading">SEP-8 Action Required</Heading2>

<div className="ModalBody">
<div className="ModalMessage">
<p>{message}</p>
</div>

<div className="ModalMessage">
<p>The following information is needed before we can proceed:</p>
</div>

{actionFields.map((fieldName) => (
<Input
key={fieldName}
id={`sep8-action-field-${fieldName}`}
label={fieldName}
onChange={(e) =>
handleOnChangeField({ fieldName, fieldValue: e.target.value })
}
value={fieldValues[fieldName] || ""}
/>
))}

{sep8Send.errorString && (
<div className="ModalMessage error">
<p>{sep8Send.errorString}</p>
</div>
)}
</div>

<div className="ModalButtonsFooter">
{sep8Send.status === ActionStatus.PENDING && <Loader />}

<Button
onClick={handleSubmitActionRequiredFields}
disabled={sep8Send.status === ActionStatus.PENDING}
>
Submit
</Button>
</div>
</>
);

return (
<Modal onClose={onClose} visible>
{renderSendPayment()}
</Modal>
);
};
39 changes: 17 additions & 22 deletions src/components/Sep8Send/Sep8Approval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,38 @@ import {
} from "ducks/sep8Send";
import { getNetworkConfig } from "helpers/getNetworkConfig";
import { useRedux } from "hooks/useRedux";
import { ActionStatus } from "types/types.d";
import { ActionStatus, Sep8Step } from "types/types.d";

export const Sep8Approval = ({ onClose }: { onClose: () => void }) => {
const { account, sep8Send, settings } = useRedux(
"account",
"sep8Send",
"settings",
);
const dispatch = useDispatch();

// form data
const [amount, setAmount] = useState("");
const [destination, setDestination] = useState("");
const {
approvalCriteria,
approvalServer,
assetCode,
assetIssuer,
} = sep8Send.data;
const [amount, setAmount] = useState(sep8Send.data.revisedTransaction.amount);
const [destination, setDestination] = useState(
sep8Send.data.revisedTransaction.destination,
);
const [isDestinationFunded, setIsDestinationFunded] = useState(true);
const dispatch = useDispatch();

const resetFormState = () => {
setDestination("");
setAmount("");
setIsDestinationFunded(true);
};

const {
approvalCriteria,
approvalServer,
assetCode,
assetIssuer,
} = sep8Send.data;
useEffect(() => {
if (sep8Send.data.sep8Step === Sep8Step.PENDING) {
onClose();
}
}, [onClose, sep8Send.data.sep8Step]);

// user interaction handlers
const handleSubmitPayment = () => {
Expand All @@ -67,16 +72,6 @@ export const Sep8Approval = ({ onClose }: { onClose: () => void }) => {
onClose();
};

// use effect
useEffect(() => {
if (
sep8Send.status === ActionStatus.CAN_PROCEED &&
sep8Send.data.revisedTransaction.revisedTxXdr
) {
resetFormState();
}
}, [sep8Send.status, sep8Send.data.revisedTransaction.revisedTxXdr]);

// helper function(s)
const checkAndSetIsDestinationFunded = async () => {
if (!destination || !StrKey.isValidEd25519PublicKey(destination)) {
Expand Down
7 changes: 4 additions & 3 deletions src/components/Sep8Send/Sep8Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fetchAccountAction } from "ducks/account";
import { sep8SubmitRevisedTransactionAction } from "ducks/sep8Send";
import { getNetworkConfig } from "helpers/getNetworkConfig";
import { useRedux } from "hooks/useRedux";
import { ActionStatus } from "types/types.d";
import { ActionStatus, Sep8Step } from "types/types.d";

export const Sep8Review = ({ onClose }: { onClose: () => void }) => {
const { account, sep8Send, settings } = useRedux(
Expand All @@ -22,6 +22,7 @@ export const Sep8Review = ({ onClose }: { onClose: () => void }) => {
const [isApproved, setIsApproved] = useState(false);
const dispatch = useDispatch();
const { revisedTxXdr, submittedTxXdr } = sep8Send.data.revisedTransaction;
const { sep8Step } = sep8Send.data;

// user interaction handlers
const handleSubmitPayment = () => {
Expand All @@ -32,7 +33,7 @@ export const Sep8Review = ({ onClose }: { onClose: () => void }) => {

// use effect: complete action, close modal and refresh account balances
useEffect(() => {
if (sep8Send.status === ActionStatus.SUCCESS && account.data?.id) {
if (sep8Step === Sep8Step.COMPLETE && account.data?.id) {
dispatch(
fetchAccountAction({
publicKey: account.data.id,
Expand All @@ -41,7 +42,7 @@ export const Sep8Review = ({ onClose }: { onClose: () => void }) => {
);
onClose();
}
}, [account.data?.id, account.secretKey, sep8Send.status, dispatch, onClose]);
}, [account.data?.id, account.secretKey, dispatch, onClose, sep8Step]);

// use effect: parse transaction XDRs
useEffect(() => {
Expand Down
40 changes: 14 additions & 26 deletions src/components/Sep8Send/index.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,36 @@
import { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { Sep8ActionRequiredForm } from "components/Sep8Send/Sep8ActionRequiredForm";
import { Sep8Approval } from "components/Sep8Send/Sep8Approval";
import { Sep8Review } from "components/Sep8Send/Sep8Review";
import { resetActiveAssetAction } from "ducks/activeAsset";
import { resetSep8SendAction } from "ducks/sep8Send";
import { useRedux } from "hooks/useRedux";
import { ActionStatus } from "types/types.d";
import { Sep8Step } from "types/types.d";

export const Sep8Send = () => {
const { sep8Send } = useRedux("sep8Send");
const [approvalModalVisible, setApprovalModalVisible] = useState(false);
const [reviewModalVisible, setReviewModalVisible] = useState(false);
const sep8Step = sep8Send.data.sep8Step;
const dispatch = useDispatch();

const onClose = () => {
setApprovalModalVisible(false);
dispatch(resetActiveAssetAction());
dispatch(resetSep8SendAction());
};

// use effect
useEffect(() => {
if (!sep8Send.status) {
setReviewModalVisible(false);
setApprovalModalVisible(false);
return;
}

if (sep8Send.status !== ActionStatus.CAN_PROCEED) {
return;
}

const hasTxToRevise = Boolean(
sep8Send.data.revisedTransaction.revisedTxXdr,
);
setApprovalModalVisible(!hasTxToRevise);
setReviewModalVisible(hasTxToRevise);
}, [sep8Send.status, sep8Send.data.revisedTransaction.revisedTxXdr]);

return (
<>
{approvalModalVisible && <Sep8Approval onClose={onClose} />}
{[Sep8Step.STARTING, Sep8Step.PENDING].includes(sep8Step) && (
<Sep8Approval onClose={onClose} />
)}

{[Sep8Step.TRANSACTION_REVISED, Sep8Step.COMPLETE].includes(sep8Step) && (
<Sep8Review onClose={onClose} />
)}

{reviewModalVisible && <Sep8Review onClose={onClose} />}
{[
Sep8Step.ACTION_REQUIRED,
Sep8Step.SENT_ACTION_REQUIRED_FIELDS,
].includes(sep8Step) && <Sep8ActionRequiredForm onClose={onClose} />}
</>
);
};
Loading

0 comments on commit 619dbcc

Please sign in to comment.