diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e6cfe8..80646ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). > Place unreleased changes here. +## [1.0.0](https://github.com/stellar/stellar-disbursement-platform-backend/compare/1.0.0-rc2...1.0.0) + +### Added + +- Add a new screen to manage Wallet Providers. + [#14](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/14) +- Add re-send SMS invitation functionality. + [#18](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/18) +- Customize receiver wallet invite SMS message. + [#17](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/17) +- Display asset issuer for Trustlines in the Distribution account screen + [#20](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/20) +- Settings: configure SMS retry interval + [#28](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/28) + +### Changed + +- Change payment status history sort order to descending order. + [#15](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/15) +- Filter assets based on wallet selection in New Disbursement screen. + [#24](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/24) +- Only show enabled wallets in the New Disbursement screen. + [#29](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/29) + +### Security + +- Add warning message about Distribution account funds + [#11](https://github.com/stellar/stellar-disbursement-platform-frontend/pull/11) + ## [1.0.0.rc2](https://github.com/stellar/stellar-disbursement-platform-backend/compare/1.0.0-rc1...1.0.0-rc2) ### Added diff --git a/package.json b/package.json index 3b0d527..2d99038 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stellar-disbursement-platform-frontend", - "version": "1.0.0-rc2", + "version": "1.0.0", "license": "Apache-2.0", "engines": { "node": ">=18.x" diff --git a/src/apiQueries/useUpdateOrgSmsRetryInterval.ts b/src/apiQueries/useUpdateOrgSmsRetryInterval.ts new file mode 100644 index 0000000..d70105c --- /dev/null +++ b/src/apiQueries/useUpdateOrgSmsRetryInterval.ts @@ -0,0 +1,37 @@ +import { useMutation } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { AppError } from "types"; + +export const useUpdateOrgSmsRetryInterval = () => { + const mutation = useMutation({ + mutationFn: (retryInterval: number) => { + const formData = new FormData(); + + formData.append("data", `{"sms_resend_interval": ${retryInterval}}`); + + return fetchApi( + `${API_URL}/organization`, + { + method: "PATCH", + body: formData, + }, + { omitContentType: true }, + ); + }, + cacheTime: 0, + }); + + return { + ...mutation, + error: mutation.error as AppError, + data: mutation.data as { message: string }, + mutateAsync: async (retryInterval: number) => { + try { + await mutation.mutateAsync(retryInterval); + } catch (e) { + // do nothing + } + }, + }; +}; diff --git a/src/components/SettingsEnableSmsRetry.tsx b/src/components/SettingsEnableSmsRetry.tsx new file mode 100644 index 0000000..1ce75b7 --- /dev/null +++ b/src/components/SettingsEnableSmsRetry.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState } from "react"; +import { + Button, + Card, + Input, + Notification, + Toggle, + Loader, +} from "@stellar/design-system"; +import { useDispatch } from "react-redux"; + +import { DropdownMenu } from "components/DropdownMenu"; +import { MoreMenuButton } from "components/MoreMenuButton"; + +import { useUpdateOrgSmsRetryInterval } from "apiQueries/useUpdateOrgSmsRetryInterval"; +import { useRedux } from "hooks/useRedux"; +import { AppDispatch } from "store"; +import { getOrgInfoAction } from "store/ducks/organization"; + +export const SettingsEnableSmsRetry = () => { + const { organization } = useRedux("organization"); + + const [smsRetryInterval, setSmsRetryInterval] = useState(0); + const [isEditMode, setIsEditMode] = useState(false); + + const dispatch: AppDispatch = useDispatch(); + + const { mutateAsync, isLoading, error, isSuccess } = + useUpdateOrgSmsRetryInterval(); + + useEffect(() => { + setSmsRetryInterval(organization.data.smsResendInterval); + }, [organization.data.smsResendInterval]); + + useEffect(() => { + if (isSuccess) { + dispatch(getOrgInfoAction()); + setIsEditMode(false); + } + }, [dispatch, isSuccess]); + + const handleToggleChange = () => { + // Default interval is 2 days + mutateAsync(organization.data.smsResendInterval === 0 ? 2 : 0); + }; + + const handleSmsRetrySubmit = (event: React.FormEvent) => { + event.preventDefault(); + mutateAsync(smsRetryInterval); + }; + + const handleSmsRetryReset = (event: React.FormEvent) => { + event.preventDefault(); + setIsEditMode(false); + setSmsRetryInterval(organization.data.smsResendInterval); + }; + + const renderContent = () => { + return ( +
+
+
+ +
+ {isLoading ? : null} + +
+
+
+ Select this option to automatically re-send the SMS invitation to + unregistered receivers after a certain time period. They will + receive the same message again. The message will only go to + receivers who have not registered their wallet. +
+
+ + {organization.data.smsResendInterval ? ( +
+
+
+ setSmsRetryInterval(Number(e.target.value))} + disabled={!isEditMode} + error={ + smsRetryInterval === 0 ? "Retry interval cannot be 0" : "" + } + /> + {!isEditMode ? ( + }> + setIsEditMode(true)}> + Edit + + + ) : null} +
+ {isEditMode ? ( +
+ + +
+ ) : null} +
+
+ ) : null} +
+ ); + }; + + return ( + <> + {error ? ( + + {error.message} + + ) : null} + + +
{renderContent()}
+
+ + ); +}; diff --git a/src/helpers/validateNewPassword.ts b/src/helpers/validateNewPassword.ts index 1c2cee9..77c4c87 100644 --- a/src/helpers/validateNewPassword.ts +++ b/src/helpers/validateNewPassword.ts @@ -7,8 +7,8 @@ export const validateNewPassword = (password: string): string => { if (!password) { errorMsg = "Password is required"; - } else if (password.length < 8) { - errorMsg = "Password must be at least 8 characters long"; + } else if (password.length < 12) { + errorMsg = "Password must be at least 12 characters long"; } else if (!passwordStrength.test(password)) { errorMsg = "Password must have at least one uppercase letter, lowercase letter, number, and symbol."; diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx index 2f5f0a6..ba6877d 100644 --- a/src/pages/ResetPassword.tsx +++ b/src/pages/ResetPassword.tsx @@ -81,8 +81,16 @@ export const ResetPassword = () => { {forgotPassword.errorString && ( - {forgotPassword.errorString}. Check your email for the correct - token. + {forgotPassword.errorString} + {forgotPassword.errorExtras ? ( +
    + {Object.entries(forgotPassword.errorExtras).map( + ([key, value]) => ( +
  • {`${key}: ${value}`}
  • + ), + )} +
+ ) : null}
)} @@ -95,7 +103,7 @@ export const ResetPassword = () => {
New password must be:
    -
  • at least 8 characters long,
  • +
  • at least 12 characters long,
  • a combination of uppercase letters, lowercase letters, numbers, and symbols. diff --git a/src/pages/SetNewPassword.tsx b/src/pages/SetNewPassword.tsx index dd11100..8f6c63f 100644 --- a/src/pages/SetNewPassword.tsx +++ b/src/pages/SetNewPassword.tsx @@ -117,7 +117,7 @@ export const SetNewPassword = () => {
    New password must be:
      -
    • at least 8 characters long,
    • +
    • at least 12 characters long,
    • a combination of uppercase letters, lowercase letters, numbers, and symbols. diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 727bf81..b484a56 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -5,6 +5,7 @@ import { SectionHeader } from "components/SectionHeader"; import { NotificationWithButtons } from "components/NotificationWithButtons"; import { SettingsTeamMembers } from "components/SettingsTeamMembers"; import { ReceiverInviteMessage } from "components/ReceiverInviteMessage"; +import { SettingsEnableSmsRetry } from "components/SettingsEnableSmsRetry"; import { AppDispatch } from "store"; import { resetNewUserAction, resetUpdatedUserAction } from "store/ducks/users"; @@ -61,6 +62,13 @@ export const Settings = () => {
      + {/* Enable SMS retry */} + + + {/* Customize receiver wallet invite */} + + + {/* Team members */} {users.updatedUser.status === "SUCCESS" ? ( { ) : null} - - {users.errorString ? ( {users.errorString} diff --git a/src/store/ducks/organization.ts b/src/store/ducks/organization.ts index 1914292..86f936d 100644 --- a/src/store/ducks/organization.ts +++ b/src/store/ducks/organization.ts @@ -125,6 +125,7 @@ const initialState: OrganizationInitialState = { timezoneUtcOffset: "", assetBalances: undefined, isApprovalRequired: undefined, + smsResendInterval: 0, }, updateMessage: undefined, status: undefined, @@ -161,6 +162,7 @@ const organizationSlice = createSlice({ isApprovalRequired: action.payload.is_approval_required, smsRegistrationMessageTemplate: action.payload.sms_registration_message_template, + smsResendInterval: Number(action.payload.sms_resend_interval || 0), }; state.status = "SUCCESS"; }); diff --git a/src/styles.scss b/src/styles.scss index 6a0e070..86d6946 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -587,6 +587,57 @@ body { } } +// Settings +.SdpSettings { + display: flex; + flex-direction: column; + gap: pxToRem(16px); + + &__row { + display: flex; + flex-direction: column; + } + + &__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: pxToRem(16px); + } + + &__label { + font-size: pxToRem(14px); + } + + &__form { + width: 100%; + display: flex; + flex-direction: column; + gap: pxToRem(16px); + + .Floater { + height: pxToRem(36px); + width: pxToRem(24px); + display: flex; + align-items: center; + justify-content: flex-end; + } + + &__row { + display: flex; + justify-content: space-between; + align-items: flex-end; + } + + &__buttons { + display: flex; + justify-content: flex-end; + align-items: center; + gap: pxToRem(16px); + } + } +} + // Not found / 404 page .NotFoundPage { display: flex; @@ -682,4 +733,15 @@ body { } } } + + // TODO: add toggle loader to SDS + .Toggle__wrapper { + display: flex; + align-items: center; + gap: pxToRem(4px); + + .Loader { + --Loader-color: var(--color-gray-50); + } + } } diff --git a/src/types/index.ts b/src/types/index.ts index fc5f042..6c8d23c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -151,6 +151,7 @@ export type OrganizationInitialState = { timezoneUtcOffset: string; assetBalances?: StellarAccountInfo[]; isApprovalRequired: boolean | undefined; + smsResendInterval: number; smsRegistrationMessageTemplate?: string; }; updateMessage?: string; @@ -840,6 +841,7 @@ export type ApiOrgInfo = { distribution_account_public_key: string; timezone_utc_offset: string; is_approval_required: boolean; + sms_resend_interval: string; sms_registration_message_template?: string; };