From 0b99bd01db9a22dd886c93764c18736303a74b1e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Jul 2023 20:30:13 +0000
Subject: [PATCH 01/13] chore(deps-dev): bump eslint-config-prettier from 8.8.0
to 8.9.0 in /shared (#6576)
chore(deps-dev): bump eslint-config-prettier in /shared
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.8.0 to 8.9.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.8.0...v8.9.0)
---
updated-dependencies:
- dependency-name: eslint-config-prettier
dependency-type: direct:development
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
shared/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/shared/package-lock.json b/shared/package-lock.json
index 39a5553d2a..11e6c1286b 100644
--- a/shared/package-lock.json
+++ b/shared/package-lock.json
@@ -372,9 +372,9 @@
}
},
"node_modules/eslint-config-prettier": {
- "version": "8.8.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
- "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.9.0.tgz",
+ "integrity": "sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
@@ -1114,9 +1114,9 @@
}
},
"eslint-config-prettier": {
- "version": "8.8.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
- "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.9.0.tgz",
+ "integrity": "sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==",
"dev": true
},
"eslint-plugin-prettier": {
From bd4915522a833c81a5a44e20d300bc5706f9ec32 Mon Sep 17 00:00:00 2001
From: wanlingt <56983748+wanlingt@users.noreply.github.com>
Date: Fri, 28 Jul 2023 17:02:59 +0800
Subject: [PATCH 02/13] ref: remove download receipt URL (#6577)
* ref: remove download receipt URL
* test: remove downloadPaymentReceipt test
---
.../components/DownloadReceiptBlock.tsx | 21 +-----
.../src/features/public-form/utils/urls.ts | 7 --
.../__tests__/stripe.controller.spec.ts | 44 ------------
src/app/modules/payments/stripe.controller.ts | 72 -------------------
.../routes/api/v3/payments/payments.routes.ts | 15 +---
5 files changed, 2 insertions(+), 157 deletions(-)
diff --git a/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/DownloadReceiptBlock.tsx b/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/DownloadReceiptBlock.tsx
index 0304ac64f8..8b16156b2f 100644
--- a/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/DownloadReceiptBlock.tsx
+++ b/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/DownloadReceiptBlock.tsx
@@ -3,10 +3,7 @@ import { BiDownload } from 'react-icons/bi'
import { useToast } from '~hooks/useToast'
import Button from '~components/Button'
-import {
- getPaymentInvoiceDownloadUrl,
- getPaymentReceiptDownloadUrl,
-} from '~features/public-form/utils/urls'
+import { getPaymentInvoiceDownloadUrl } from '~features/public-form/utils/urls'
import { GenericMessageBlock } from './GenericMessageBlock'
@@ -23,13 +20,6 @@ export const DownloadReceiptBlock = ({
}: DownloadReceiptBlockProps) => {
const toast = useToast({ status: 'success', isClosable: true })
- const handleReceiptClick = () => {
- toast({
- description: 'Receipt download started',
- })
- window.location.href = getPaymentReceiptDownloadUrl(formId, paymentId)
- }
-
const handleInvoiceClick = () => {
toast({
description: 'Proof of payment download started',
@@ -43,15 +33,6 @@ export const DownloadReceiptBlock = ({
submissionId={submissionId}
>
<>
- }
- onClick={handleReceiptClick}
- >
- Save payment receipt
-
}
diff --git a/frontend/src/features/public-form/utils/urls.ts b/frontend/src/features/public-form/utils/urls.ts
index bb56afe563..4bc2075bd9 100644
--- a/frontend/src/features/public-form/utils/urls.ts
+++ b/frontend/src/features/public-form/utils/urls.ts
@@ -6,13 +6,6 @@ export const getPaymentPageUrl = (formId: string, paymentId: string) => {
return `/${formId}/payment/${paymentId}` as const
}
-export const getPaymentReceiptDownloadUrl = (
- formId: string,
- paymentId: string,
-) => {
- return `${API_BASE_URL}/payments/${formId}/${paymentId}/receipt/download` as const
-}
-
export const getPaymentInvoiceDownloadUrl = (
formId: string,
paymentId: string,
diff --git a/src/app/modules/payments/__tests__/stripe.controller.spec.ts b/src/app/modules/payments/__tests__/stripe.controller.spec.ts
index cabea5bd24..5f1ccfd931 100644
--- a/src/app/modules/payments/__tests__/stripe.controller.spec.ts
+++ b/src/app/modules/payments/__tests__/stripe.controller.spec.ts
@@ -192,50 +192,6 @@ describe('stripe.controller', () => {
})
})
- describe('downloadReceiptInvoice', () => {
- beforeEach(async () => {
- await dbHandler.clearCollection(Payment.collection.name)
- await dbHandler.clearCollection(EncryptPendingSubmission.collection.name)
- })
- it('should generate return a pdf file when receipt url is present', async () => {
- const pendingSubmission = await EncryptPendingSubmission.create({
- submissionType: SubmissionType.Encrypt,
- form: MOCK_FORM_ID,
- encryptedContent: 'some random encrypted content',
- version: 1,
- })
- const payment = await Payment.create({
- formId: MOCK_FORM_ID,
- targetAccountId: 'acct_MOCK_ACCOUNT_ID',
- pendingSubmissionId: pendingSubmission._id,
- amount: 12345,
- status: PaymentStatus.Succeeded,
- paymentIntentId: 'pi_MOCK_PAYMENT_INTENT',
- email: 'formsg@tech.gov.sg',
- completedPayment: {
- receiptUrl: 'http://form.gov.sg',
- },
- gstEnabled: false,
- })
-
- const mockReq = expressHandler.mockRequest({
- params: { formId: 'test@example.com', paymentId: payment._id },
- })
- const mockRes = expressHandler.mockResponse()
- const axiosSpy = jest
- .spyOn(axios, 'get')
- .mockResolvedValueOnce({ data: '>some html' })
-
- // Act
- await StripeController.downloadPaymentReceipt(mockReq, mockRes, jest.fn())
-
- // Assert
- expect(mockRes.status).toHaveBeenCalledWith(200)
- expect(mockRes.send).toHaveBeenCalledOnce()
- expect(axiosSpy).toHaveBeenCalledOnce()
- })
- })
-
describe('_handleConnectOauthCallback', () => {
beforeEach(async () => {
await dbHandler.clearCollection(Payment.collection.name)
diff --git a/src/app/modules/payments/stripe.controller.ts b/src/app/modules/payments/stripe.controller.ts
index b86497636c..3461eafc93 100644
--- a/src/app/modules/payments/stripe.controller.ts
+++ b/src/app/modules/payments/stripe.controller.ts
@@ -1,6 +1,5 @@
// Use 'stripe-event-types' for better type discrimination.
///
-import axios from 'axios'
import { celebrate, Joi, Segments } from 'celebrate'
import { StatusCodes } from 'http-status-codes'
import get from 'lodash/get'
@@ -18,7 +17,6 @@ import config from '../../config/config'
import { paymentConfig } from '../../config/features/payment.config'
import { createLoggerWithLabel } from '../../config/logger'
import { stripe } from '../../loaders/stripe'
-import { generatePdfFromHtml } from '../../utils/convert-html-to-pdf'
import { createReqMeta } from '../../utils/request'
import { ControllerHandler } from '../core/core.types'
import * as FormService from '../form/form.service'
@@ -221,76 +219,6 @@ export const downloadPaymentInvoice: ControllerHandler<{
})
}
-/**
- * Handler for GET /api/v3/payments/:formId/:paymentId/invoice/download
- * Receives Stripe webhooks and updates the database with transaction details.
- *
- * @returns 200 if webhook is successfully processed
- * @returns 404 if the PaymentId is not found
- * @returns 404 if the FormId is not found
- * @returns 404 if payment.completedPayment?.receiptUrl is not found
- */
-export const downloadPaymentReceipt: ControllerHandler<{
- formId: string
- paymentId: string
-}> = (req, res) => {
- const { formId, paymentId } = req.params
- logger.info({
- message: 'downloadPaymentReceipt endpoint called',
- meta: {
- action: 'downloadPaymentReceipt',
- formId,
- paymentId,
- },
- })
-
- return PaymentService.findPaymentById(paymentId)
- .map((payment) => {
- logger.info({
- message: 'Found paymentId in payment document',
- meta: {
- action: 'downloadPaymentReceipt',
- payment,
- },
- })
- if (!payment.completedPayment?.receiptUrl) {
- return res
- .status(StatusCodes.NOT_FOUND)
- .send({ message: 'Receipt url not ready' })
- }
- // retrieve receiptURL as html
- return (
- axios
- .get(payment.completedPayment.receiptUrl)
- // convert to pdf and return
- .then((receiptUrlResponse) => {
- const html = receiptUrlResponse.data
- const pdfBufferPromise = generatePdfFromHtml(html)
- return pdfBufferPromise
- })
- .then((pdfBuffer) => {
- res.set({
- 'Content-Type': 'application/pdf',
- 'Content-Disposition': `attachment; filename=${paymentId}-receipt.pdf`,
- })
- return res.status(StatusCodes.OK).send(pdfBuffer)
- })
- )
- })
- .mapErr((error) => {
- logger.error({
- message: 'Error retrieving receipt',
- meta: {
- action: 'downloadPaymentReceipt',
- formId,
- paymentId,
- },
- error,
- })
- return res.status(StatusCodes.NOT_FOUND).json({ message: error })
- })
-}
-
const _handleConnectOauthCallback: ControllerHandler<
unknown,
unknown,
diff --git a/src/app/routes/api/v3/payments/payments.routes.ts b/src/app/routes/api/v3/payments/payments.routes.ts
index 03ff819b07..19c27e72d9 100644
--- a/src/app/routes/api/v3/payments/payments.routes.ts
+++ b/src/app/routes/api/v3/payments/payments.routes.ts
@@ -20,24 +20,11 @@ PaymentsRouter.get(
StripeController.checkPaymentReceiptStatus,
)
-/**
- * Downloads the receipt pdf
- * @route GET /payments/:formId/:paymentId/receipt/download
- *
- * @returns 200 with receipt attatchment as content in PDF
- * @returns 404 if receipt url doesn't exist or payment does not exist
- */
-PaymentsRouter.get(
- '/:formId([a-fA-F0-9]{24})/:paymentId([a-fA-F0-9]{24})/receipt/download',
- limitRate({ max: rateLimitConfig.downloadPaymentReceipt }),
- StripeController.downloadPaymentReceipt,
-)
-
/**
* Downloads the invoice pdf
* @route GET /payments/:formId/:paymentId/invoice/download
*
- * @returns 200 with receipt attatchment as content in PDF
+ * @returns 200 with receipt attachment as content in PDF
* @returns 404 if receipt url doesn't exist or payment does not exist
*/
PaymentsRouter.get(
From 6ad44c20d6d9955940bb4e5c93d63a3673946f4f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 30 Jul 2023 19:35:24 +0000
Subject: [PATCH 03/13] fix(deps): bump libphonenumber-js from 1.10.37 to
1.10.38 in /shared (#6580)
Bumps [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js) from 1.10.37 to 1.10.38.
- [Changelog](https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/catamphetamine/libphonenumber-js/compare/v1.10.37...v1.10.38)
---
updated-dependencies:
- dependency-name: libphonenumber-js
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
shared/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/shared/package-lock.json b/shared/package-lock.json
index 11e6c1286b..57c350b013 100644
--- a/shared/package-lock.json
+++ b/shared/package-lock.json
@@ -608,9 +608,9 @@
}
},
"node_modules/libphonenumber-js": {
- "version": "1.10.37",
- "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.37.tgz",
- "integrity": "sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw=="
+ "version": "1.10.38",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.38.tgz",
+ "integrity": "sha512-4NjVXVUmpZ9Zsqq6FXa2+MKI+KAI3tOqA0pxXgXGluhpj4ge5didmbWJpMBqGB3AVGv1SnEtKdGTbxjSEG1kCQ=="
},
"node_modules/lie": {
"version": "3.3.0",
@@ -1292,9 +1292,9 @@
}
},
"libphonenumber-js": {
- "version": "1.10.37",
- "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.37.tgz",
- "integrity": "sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw=="
+ "version": "1.10.38",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.38.tgz",
+ "integrity": "sha512-4NjVXVUmpZ9Zsqq6FXa2+MKI+KAI3tOqA0pxXgXGluhpj4ge5didmbWJpMBqGB3AVGv1SnEtKdGTbxjSEG1kCQ=="
},
"lie": {
"version": "3.3.0",
From 18e3723940238f84d55dc926e2e835445064f26b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 31 Jul 2023 10:08:28 +0800
Subject: [PATCH 04/13] fix(deps): bump type-fest from 3.13.0 to 4.1.0 in
/shared (#6579)
Bumps [type-fest](https://github.com/sindresorhus/type-fest) from 3.13.0 to 4.1.0.
- [Release notes](https://github.com/sindresorhus/type-fest/releases)
- [Commits](https://github.com/sindresorhus/type-fest/compare/v3.13.0...v4.1.0)
---
updated-dependencies:
- dependency-name: type-fest
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
shared/package-lock.json | 16 ++++++++--------
shared/package.json | 2 +-
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/shared/package-lock.json b/shared/package-lock.json
index 57c350b013..1c90e3da75 100644
--- a/shared/package-lock.json
+++ b/shared/package-lock.json
@@ -13,7 +13,7 @@
"jszip": "^3.7.1",
"libphonenumber-js": "^1.9.43",
"lodash": "^4.17.21",
- "type-fest": "^3.2.0",
+ "type-fest": "^4.1.0",
"validator": "^13.7.0",
"zod": "^3.11.6"
},
@@ -862,11 +862,11 @@
}
},
"node_modules/type-fest": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
- "integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.1.0.tgz",
+ "integrity": "sha512-VJGJVepayd8OWavP+rgXt4i3bfLk+tSomTV7r4mca2XD/oTCWnkJlNkpXavkxdmtU2aKdAmFGeHvoQutOVHCZg==",
"engines": {
- "node": ">=14.16"
+ "node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -1475,9 +1475,9 @@
}
},
"type-fest": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
- "integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A=="
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.1.0.tgz",
+ "integrity": "sha512-VJGJVepayd8OWavP+rgXt4i3bfLk+tSomTV7r4mca2XD/oTCWnkJlNkpXavkxdmtU2aKdAmFGeHvoQutOVHCZg=="
},
"util-deprecate": {
"version": "1.0.2",
diff --git a/shared/package.json b/shared/package.json
index e0dc092b53..4135e734fe 100644
--- a/shared/package.json
+++ b/shared/package.json
@@ -8,7 +8,7 @@
"jszip": "^3.7.1",
"libphonenumber-js": "^1.9.43",
"lodash": "^4.17.21",
- "type-fest": "^3.2.0",
+ "type-fest": "^4.1.0",
"validator": "^13.7.0",
"zod": "^3.11.6"
},
From c24ef55b4c5f5c113735dfd9215db601573ca7a5 Mon Sep 17 00:00:00 2001
From: wanlingt <56983748+wanlingt@users.noreply.github.com>
Date: Tue, 1 Aug 2023 17:07:50 +0800
Subject: [PATCH 05/13] feat: remove divider in payments input panel (#6586)
---
.../PaymentsInputPanel/PaymentsInputPanel.tsx | 54 ++++++++++---------
1 file changed, 28 insertions(+), 26 deletions(-)
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/PaymentsInputPanel.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/PaymentsInputPanel.tsx
index bf58a58545..ab3c8c4b5f 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/PaymentsInputPanel.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/PaymentsInputPanel.tsx
@@ -7,7 +7,7 @@ import {
} from 'react-hook-form'
import { Link as ReactLink } from 'react-router-dom'
import { useDebounce } from 'react-use'
-import { Box, FormControl, Link, Text, Textarea } from '@chakra-ui/react'
+import { Box, FormControl, Link, Stack, Text, Textarea } from '@chakra-ui/react'
import { cloneDeep } from 'lodash'
import {
@@ -204,31 +204,33 @@ const PaymentInput = ({
)}
/>
-
-
- Product/service name
-
-
- {errors.name?.message}
-
-
- Description
-
- {errors.description?.message}
-
+
+
+
+ Product/service name
+
+
+ {errors.name?.message}
+
+
+ Description
+
+ {errors.description?.message}
+
+
{paymentsData?.payment_type === PaymentType.Variable ? (
Date: Tue, 1 Aug 2023 18:51:47 +0800
Subject: [PATCH 06/13] feat: payments UI/UX clean up (#6581)
* feat: add helper text to variable payments field for min and max val
* feat: update payment settings page copy
* feat: misc copy changes
* fix: increase spacing between 'Payments' header and first element
* fix: update stripe onboarding guide link
* feat: change description from textarea to input
* fix: copy changes
* feat: remove placeholder from business address and gst reg no inputs
* feat: reorder payment item description
* feat: change button copy for paynow option
* fix: remove console.log
* fix: remove unused import
---
frontend/src/constants/links.ts | 3 +-
.../FixedPaymentAmountField.tsx | 2 +-
.../PaymentsInputPanel/PaymentsInputPanel.tsx | 4 +--
.../VariablePaymentAmountField.tsx | 7 ++--
.../common/utils/getPaymentDataView.ts | 3 +-
.../BusinessInfoSection.tsx | 14 ++++----
.../PaymentSettingsSection.tsx | 34 ++++++++++++-------
.../PaymentsUnsupportedMsg.tsx | 2 +-
.../StripeConnectButton.tsx | 2 +-
.../components/PaymentItemNameDescription.tsx | 4 ++-
.../VariablePaymentItemDetailBlock.tsx | 8 ++++-
.../stripe/components/PaymentBlock.tsx | 15 ++++++--
.../rollout-announcement/Announcements.tsx | 2 +-
.../features/whats-new/FeatureUpdateList.ts | 2 +-
.../components/WorkspacePageContent.tsx | 2 +-
.../src/pages/Landing/Home/LandingPage.tsx | 4 +--
.../Landing/Payments/LandingPaymentsPage.tsx | 10 +++---
.../Field/PaymentPreview/PaymentPreview.tsx | 2 +-
src/app/services/mail/mail.utils.ts | 6 ++--
19 files changed, 76 insertions(+), 50 deletions(-)
diff --git a/frontend/src/constants/links.ts b/frontend/src/constants/links.ts
index 719942d8a0..52b38617d2 100644
--- a/frontend/src/constants/links.ts
+++ b/frontend/src/constants/links.ts
@@ -31,8 +31,7 @@ export const GUIDE_PREVENT_EMAIL_BOUNCE =
export const GUIDE_EMAIL_RELIABILITY =
'https://go.gov.sg/formsg-guide-email-reliability'
export const GUIDE_PREFILL = 'https://go.gov.sg/formsg-guide-prefills'
-export const GUIDE_STRIPE_ONBOARDING =
- 'https://go.gov.sg/formsg-stripe-onboarding'
+export const GUIDE_STRIPE_ONBOARDING = 'https://go.gov.sg/formsg-payments'
export const GUIDE_PAYMENTS_ENTRY = 'https://go.gov.sg/formsg-payment-overview'
export const GUIDE_PAYMENTS_INVOICE_DIFFERENCES =
'https://go.gov.sg/formsg-payments-invoice-differences'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/FixedPaymentAmountField.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/FixedPaymentAmountField.tsx
index bc9bf363c6..59918f320d 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/FixedPaymentAmountField.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/FixedPaymentAmountField.tsx
@@ -38,7 +38,7 @@ export const FixedPaymentAmountField = ({
isInvalid={!!errors[DISPLAY_AMOUNT_FIELD_KEY]}
isRequired
>
-
+
Payment amount
Description
-
+
{errors.description?.message}
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/VariablePaymentAmountField.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/VariablePaymentAmountField.tsx
index bce9785704..3d9833f41e 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/VariablePaymentAmountField.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/field-panels/PaymentsInputPanel/VariablePaymentAmountField.tsx
@@ -59,10 +59,11 @@ export const VariablePaymentAmountField = ({
isReadOnly={isLoading}
// these invalid checks are required to trigger FormErrorMessage to display
isInvalid={!!errors[MIN_FIELD_KEY]?.message || !!errors[MAX_FIELD_KEY]}
+ isDisabled={isDisabled}
>
Payment amount limit
@@ -71,7 +72,7 @@ export const VariablePaymentAmountField = ({
isInvalid={!!errors[MIN_FIELD_KEY]}
isDisabled={isDisabled}
>
- Minimum Amount
+ Minimum amount
- Maximum Amount
+ Maximum amount
`S$${(cents / 100).toFixed(2)}`
+export const centsToDollarString = (cents: number) =>
+ `S$${(cents / 100).toFixed(2)}`
const toSentenceCase = (str: string) =>
`${str.charAt(0).toUpperCase()}${str.substring(1).toLowerCase()}`
// replace underscores with a space
diff --git a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/BusinessInfoSection.tsx b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/BusinessInfoSection.tsx
index b03a9bceec..55c564fd66 100644
--- a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/BusinessInfoSection.tsx
+++ b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/BusinessInfoSection.tsx
@@ -24,12 +24,10 @@ import { useAdminFormSettings } from '../../queries'
interface BusinessFieldInputProps {
initialValue: string
handleMutation: (newAddress: string) => void
- placeholder: string
}
const BusinessFieldInput = ({
initialValue,
handleMutation,
- placeholder,
}: BusinessFieldInputProps): JSX.Element => {
const [value, setValue] = useState(initialValue)
@@ -66,8 +64,6 @@ const BusinessFieldInput = ({
onChange={handleValueChange}
onKeyDown={handleKeydown}
onBlur={handleBlur}
- placeholder={placeholder}
- _placeholder={{ opacity: 1 }}
/>
)
}
@@ -92,8 +88,9 @@ const BusinessInfoBlock = ({
GST Registration Number
@@ -101,8 +98,9 @@ const BusinessInfoBlock = ({
Business Address
diff --git a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentSettingsSection.tsx b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentSettingsSection.tsx
index 4a503f48e9..02eca3cc8f 100644
--- a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentSettingsSection.tsx
+++ b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentSettingsSection.tsx
@@ -7,6 +7,7 @@ import {
Icon,
Skeleton,
Text,
+ VStack,
} from '@chakra-ui/react'
import { FormResponseMode, PaymentChannel } from '~shared/types'
@@ -40,30 +41,39 @@ const BeforeConnectionInstructions = ({
const { data: paymentGuideLink } = usePaymentGuideLink()
if (isProductionEnv) {
return (
- <>
-
+
+
Read{' '}
our guide
{' '}
- to learn more about onboarding to Stripe. To enjoy bulk tender
- transaction rates,{' '}
-
- use this form
- {' '}
- to submit your Stripe account ID and raise a purchase order to
- Stripe.
+ to set up a Stripe account. If your agency already has a Stripe
+ account, you can connect it to this form.
+
+ Bulk transaction rates
+
+
+ To request bulk transaction rates for your payments, use{' '}
+
+ this form
+ {' '}
+ to contact us for assistance.{' '}
+
+ Without this step, you will be charged default transaction rates.
+
+
+
{/* Stripe connect button should only be enabled when checkbox is checked. */}
setAllowConnect(e.target.checked)}
>
- I understand that if I do not send my Stripe account ID and raise a
- purchase order to Stripe, I will be paying default transaction rates.
+ I understand that I will be paying default transaction rates, unless I
+ have requested bulk transaction rates and received confirmation
- >
+
)
} else {
return (
diff --git a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentsUnsupportedMsg.tsx b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentsUnsupportedMsg.tsx
index 00e22bcd4e..0f91aff3a0 100644
--- a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentsUnsupportedMsg.tsx
+++ b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/PaymentsUnsupportedMsg.tsx
@@ -14,7 +14,7 @@ export const PaymentsUnsupportedMsg = (): JSX.Element => {
Payments are not available in Email mode
- Citizens can now make payment for fees or services directly on your
+ Respondents can now make payment for fees or services directly on your
form. This feature is only available in Storage Mode.
Learn more about payments
diff --git a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/StripeConnectButton.tsx b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/StripeConnectButton.tsx
index 617fe1de7b..5dd89f3662 100644
--- a/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/StripeConnectButton.tsx
+++ b/frontend/src/features/admin-form/settings/components/PaymentSettingsSection/StripeConnectButton.tsx
@@ -41,7 +41,7 @@ export const StripeConnectButton = ({
onClick={onLinkAccountClick}
colorScheme="primary"
>
- Connect with my Stripe account
+ Connect my Stripe account
)
} else {
diff --git a/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentItemNameDescription.tsx b/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentItemNameDescription.tsx
index 0de8fac626..1f3e58523a 100644
--- a/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentItemNameDescription.tsx
+++ b/frontend/src/features/public-form/components/FormPaymentPage/components/PaymentItemNameDescription.tsx
@@ -15,7 +15,9 @@ const PaymentItemNameDescription = ({
{paymentItemName}
) : null}
{paymentDescription ? (
- {paymentDescription}
+
+ {paymentDescription}
+
) : null}
)
diff --git a/frontend/src/features/public-form/components/FormPaymentPage/components/VariablePaymentItemDetailBlock.tsx b/frontend/src/features/public-form/components/FormPaymentPage/components/VariablePaymentItemDetailBlock.tsx
index 634c2cce39..1e3506084a 100644
--- a/frontend/src/features/public-form/components/FormPaymentPage/components/VariablePaymentItemDetailBlock.tsx
+++ b/frontend/src/features/public-form/components/FormPaymentPage/components/VariablePaymentItemDetailBlock.tsx
@@ -1,10 +1,12 @@
import { Controller, useFormContext } from 'react-hook-form'
-import { Box, FormControl, FormErrorMessage } from '@chakra-ui/react'
+import { Box, FormControl, FormErrorMessage, Text } from '@chakra-ui/react'
import { PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID } from '~shared/constants'
import MoneyInput from '~components/MoneyInput'
+import { centsToDollarString } from '~features/admin-form/responses/common/utils/getPaymentDataView'
+
import { usePaymentFieldValidation } from '../../../../../hooks/usePaymentFieldValidation'
import PaymentItemNameDescription from './PaymentItemNameDescription'
@@ -51,6 +53,10 @@ export const VariablePaymentItemDetailsField = ({
/>
)}
/>
+
+ The minimum amount is {centsToDollarString(paymentMin)} and the
+ maximum amount is {centsToDollarString(paymentMax)}.
+
{errors[PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID]?.message}
diff --git a/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/PaymentBlock.tsx b/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/PaymentBlock.tsx
index 5ea9ce5031..c7df66919b 100644
--- a/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/PaymentBlock.tsx
+++ b/frontend/src/features/public-form/components/FormPaymentPage/stripe/components/PaymentBlock.tsx
@@ -48,6 +48,7 @@ const StripeCheckoutForm = ({
const [stripeMessage, setStripeMessage] = useState('')
const [isStripeProcessing, setIsStripeProcessing] = useState(false)
const [, , clearPaymentMemory] = useBrowserStm(formId)
+ const [isPaynow, setIsPayNow] = useState(false)
useEffect(() => {
if (isRetry) {
@@ -100,7 +101,15 @@ const StripeCheckoutForm = ({
return (
@@ -160,7 +169,7 @@ export const StripePaymentBlock = ({
{submittedAriaText}
-
+
Payment
{
<>
@@ -447,7 +447,7 @@ export const LandingPage = (): JSX.Element => {
- Build and share form link with citizens
+ Build and share form link with respondents
diff --git a/frontend/src/pages/Landing/Payments/LandingPaymentsPage.tsx b/frontend/src/pages/Landing/Payments/LandingPaymentsPage.tsx
index dcd90474f5..df0abd5389 100644
--- a/frontend/src/pages/Landing/Payments/LandingPaymentsPage.tsx
+++ b/frontend/src/pages/Landing/Payments/LandingPaymentsPage.tsx
@@ -137,7 +137,7 @@ export const LandingPaymentsPage = (): JSX.Element => {
{
- Each payment reference ID is associated with a form submission ID.
+ Each payment reference ID is associated with a form response ID.
Payment and payout status can be viewed in your form response page
as well as on the Stripe dashboard.
@@ -248,9 +248,9 @@ export const LandingPaymentsPage = (): JSX.Element => {
Collect payments on your form
- Citizens can now pay for fees and services directly on your form.
- Enter your agency email to receive our guide on how to get started
- with payments.
+ Respondents can now pay for fees and services directly on your
+ form. Enter your agency email to receive our guide on how to get
+ started with payments.
diff --git a/frontend/src/templates/Field/PaymentPreview/PaymentPreview.tsx b/frontend/src/templates/Field/PaymentPreview/PaymentPreview.tsx
index 6ef9486bf1..6edb43b249 100644
--- a/frontend/src/templates/Field/PaymentPreview/PaymentPreview.tsx
+++ b/frontend/src/templates/Field/PaymentPreview/PaymentPreview.tsx
@@ -45,7 +45,7 @@ export const PaymentPreview = ({
return (
<>
-
+
Payment
diff --git a/src/app/services/mail/mail.utils.ts b/src/app/services/mail/mail.utils.ts
index af3dc831df..392b0a2880 100644
--- a/src/app/services/mail/mail.utils.ts
+++ b/src/app/services/mail/mail.utils.ts
@@ -292,12 +292,12 @@ export const generatePaymentOnboardingHtml = ({
appName: string
}): string => {
return dedent`
- Dear Sir or Madam,
- Thank you for your interest in our payments feature! Download the file to learn how to get started with payments today!
+ Dear Form admin,
+ Thank you for your interest in our payments feature! Download our payments guide to learn how to start collecting payments on your form today!
If you have any questions regarding payments, feel free to reach out to support@form.gov.sg.
Regards,
- ${appName} team
+ ${appName}
`
}
From 6cda5314600b3d100b19f988619087545b91465a Mon Sep 17 00:00:00 2001
From: Jiayee Lim
Date: Wed, 2 Aug 2023 09:57:00 +0800
Subject: [PATCH 07/13] feat: add Country/Region field (#5132)
* feat: add country field backend (#3770)
* feat: add country field backend
* fix: Fix Property 'country' is missing
* fix: replace BasicField.Dropdown in CountryField story
* fix: import CountryField into FieldFactory
* fix: fix type errors due to lack of handling for new country field
Co-authored-by: Kar Rui
* feat: add CountryRegion field to frontend (#4112)
* feat: add country field backend (#3770)
* feat: add country field backend
* fix: Fix Property 'country' is missing
* fix: replace BasicField.Dropdown in CountryField story
* fix: import CountryField into FieldFactory
* fix: fix type errors due to lack of handling for new country field
Co-authored-by: Kar Rui
* feat: add Country field to frontend
* feat: move Singapore up to the first value in the Country enum
* test: add tests for CountryField
* fix: update the typed characters in the partial typing test case
* test: adjust test case for partial typing in Country field
* refactor: rename Country enum to CountryRegion
* refactor: rename BasicField.Country to BasicField.CountryRegion
* refactor: rename CountryField to CountryRegionField
* fix: commit renaming changes for tests/
* refactor: rename *Country* to *CountryRegion* in frontend edit field folder
* feat: sort CountryRegion enums inside CountryRegionField
* fix: remove removed isSaveEnabled prop
* fix: inline sort country options constant
* test: update CountryRegionField tests
* fix(CountryRegionField): correctly set singapore as the first option
also update tests to use the options that the field uses
* style: use flag icon and fix capitalisation for CountryRegion
* feat: allow validation rule overriding for DropdownField
* fix: update error message for invalid Country/Region
* test: fix country/region capitalisation in test
Co-authored-by: Kar Rui
* fix: remove duplicate country files
* fix: remove duplicate country field files and restore TODO
* chore: rename test file
* fix: add missing imports
* fix: use new CreatePageDrawerContentContainer component
* fix: dropdown now uses requiredSingleAnswerValidationFn for validation
* fix: add validation and processing of CountryRegion fields on client
* refactor: implement specific CRField instead of using DropdownField
* fix: remove unused field schema regression
* feat: make CountryRegion values uppercase when sent to the server
* Revert "feat: make CountryRegion values uppercase when sent to the server"
This reverts commit be8a9d39916ed032baa858cc4abdb3b2a2a67333.
* fix: use title-case country/region only in render
* Revert "fix: use title-case country/region only in render"
This reverts commit 589af797aa8a21135eb0dca0c7e72ed1e0f74a97.
* feat: transform Country/Region form inputs to upper-case on submit
* fix: validate against upper-case country/region, add comments on why this is done, and remove reference to any
* fix: remove reference to toString()
* fix: simulate transformations from handleSubmitForm in country-region-validation.spec.ts
* fix: resolve rebase and use === instead of ==
* fix: remove duplicate import
* chore: fix prettier violations
* fix: update generate-form-data import path
* fix: follow DropdownField's way of querying input element and casting options
* fix: update INVALID_COUNTRY_REGION_OPTION_ERROR
* fix: linting
* fix: use uppercased countryregion for non-fetch submission
---------
Co-authored-by: Kar Rui
Co-authored-by: Jiayee Lim
Co-authored-by: Jiayee
Co-authored-by: tshuli
---
.../backend/helpers/generate-form-data.ts | 7 +
frontend/src/constants/validation.ts | 3 +
.../FieldRow/FieldRowContainer.tsx | 3 +
.../EditFieldDrawer/EditFieldDrawer.tsx | 3 +
.../EditCountryRegion.stories.tsx | 51 ++++
.../EditCountryRegion/EditCountryRegion.tsx | 92 +++++++
.../edit-fieldtype/EditCountryRegion/index.ts | 1 +
.../EditFieldDrawer/edit-fieldtype/index.ts | 1 +
.../create/builder-and-design/constants.ts | 1 +
.../builder-and-design/utils/fieldCreation.ts | 7 +
.../features/admin-form/create/constants.ts | 6 +
.../public-form/PublicFormProvider.tsx | 42 ++-
.../components/FormFields/FieldFactory.tsx | 3 +
.../public-form/utils/createSubmission.ts | 2 +-
.../public-form/utils/inputTransformation.ts | 7 +-
.../public-form/utils/validateResponses.ts | 3 +
.../Field/CountryField/CountryField.tsx | 28 --
.../CountryRegionField.stories.tsx} | 29 +-
.../CountryRegionField.test.tsx | 140 ++++++++++
.../CountryRegionField/CountryRegionField.tsx | 63 +++++
.../Field/CountryRegionField/index.ts | 1 +
.../Field/Dropdown/DropdownField.tsx | 9 +-
frontend/src/templates/Field/index.ts | 2 +
frontend/src/utils/fieldValidation.ts | 38 ++-
shared/constants/countries.ts | 256 ------------------
shared/constants/countryRegion.ts | 256 ++++++++++++++++++
shared/constants/field/basic.ts | 6 +
shared/types/field/base.ts | 1 +
shared/types/field/countryRegionField.ts | 7 +
shared/types/field/index.ts | 4 +
shared/types/response.ts | 6 +
src/app/models/field/countryRegionField.ts | 9 +
src/app/models/field/index.ts | 2 +
src/app/models/form.server.model.ts | 5 +
.../answerValidator.factory.ts | 3 +
.../country-region-validation.spec.ts | 115 ++++++++
.../validators/countryRegionValidator.ts | 38 +++
src/types/field/countryRegionField.ts | 9 +
src/types/field/index.ts | 4 +
39 files changed, 943 insertions(+), 320 deletions(-)
create mode 100644 frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.stories.tsx
create mode 100644 frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.tsx
create mode 100644 frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/index.ts
delete mode 100644 frontend/src/templates/Field/CountryField/CountryField.tsx
rename frontend/src/templates/Field/{CountryField/CountryField.stories.tsx => CountryRegionField/CountryRegionField.stories.tsx} (76%)
create mode 100644 frontend/src/templates/Field/CountryRegionField/CountryRegionField.test.tsx
create mode 100644 frontend/src/templates/Field/CountryRegionField/CountryRegionField.tsx
create mode 100644 frontend/src/templates/Field/CountryRegionField/index.ts
delete mode 100644 shared/constants/countries.ts
create mode 100644 shared/constants/countryRegion.ts
create mode 100644 shared/types/field/countryRegionField.ts
create mode 100644 src/app/models/field/countryRegionField.ts
create mode 100644 src/app/utils/field-validation/validators/__tests__/country-region-validation.spec.ts
create mode 100644 src/app/utils/field-validation/validators/countryRegionValidator.ts
create mode 100644 src/types/field/countryRegionField.ts
diff --git a/__tests__/unit/backend/helpers/generate-form-data.ts b/__tests__/unit/backend/helpers/generate-form-data.ts
index 799dd8bb73..69a3a72c50 100644
--- a/__tests__/unit/backend/helpers/generate-form-data.ts
+++ b/__tests__/unit/backend/helpers/generate-form-data.ts
@@ -13,6 +13,7 @@ import {
IAttachmentFieldSchema,
IAttachmentResponse,
ICheckboxFieldSchema,
+ ICountryRegionFieldSchema,
IDateFieldSchema,
IDecimalFieldSchema,
IDropdownFieldSchema,
@@ -117,6 +118,12 @@ export const generateDefaultField = (
getQuestion: () => defaultParams.title,
...customParams,
} as IDropdownFieldSchema
+ case BasicField.CountryRegion:
+ return {
+ ...defaultParams,
+ getQuestion: () => defaultParams.title,
+ ...customParams,
+ } as ICountryRegionFieldSchema
case BasicField.Decimal:
return {
...defaultParams,
diff --git a/frontend/src/constants/validation.ts b/frontend/src/constants/validation.ts
index 6f788ae951..d9157c8b40 100644
--- a/frontend/src/constants/validation.ts
+++ b/frontend/src/constants/validation.ts
@@ -6,3 +6,6 @@ export const INVALID_EMAIL_DOMAIN_ERROR =
export const INVALID_DROPDOWN_OPTION_ERROR =
'Entered value is not a valid dropdown option'
+
+export const INVALID_COUNTRY_REGION_OPTION_ERROR =
+ 'Please select a valid country/region from the dropdown list'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
index 3759fc554c..9a362b47e5 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx
@@ -25,6 +25,7 @@ import {
AttachmentField,
CheckboxField,
ChildrenCompoundField,
+ CountryRegionField,
DateField,
DecimalField,
DropdownField,
@@ -482,6 +483,8 @@ const FieldRow = ({ field, ...rest }: FieldRowProps) => {
return
case BasicField.Dropdown:
return
+ case BasicField.CountryRegion:
+ return
case BasicField.ShortText:
return
case BasicField.LongText:
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
index 5dbd3889c2..aaddf23aac 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/EditFieldDrawer.tsx
@@ -28,6 +28,7 @@ import {
import {
EditAttachment,
EditCheckbox,
+ EditCountryRegion,
EditDate,
EditDecimal,
EditDropdown,
@@ -130,6 +131,8 @@ export const MemoFieldDrawerContent = memo(
return
case BasicField.Dropdown:
return
+ case BasicField.CountryRegion:
+ return
case BasicField.Mobile:
return
case BasicField.HomeNo:
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.stories.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.stories.tsx
new file mode 100644
index 0000000000..98fe7db376
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.stories.tsx
@@ -0,0 +1,51 @@
+import { Meta, Story } from '@storybook/react'
+
+import { CountryRegion } from '~shared/constants/countryRegion'
+import { BasicField, CountryRegionFieldBase } from '~shared/types'
+
+import { createFormBuilderMocks } from '~/mocks/msw/handlers/admin-form'
+
+import { EditFieldDrawerDecorator, StoryRouter } from '~utils/storybook'
+
+import { EditCountryRegion } from './EditCountryRegion'
+
+const DEFAULT_COUNTRY_REGION_FIELD: CountryRegionFieldBase = {
+ title: 'Storybook Country/Region',
+ description: 'Some description about Country/Region',
+ required: true,
+ disabled: false,
+ fieldType: BasicField.CountryRegion,
+ fieldOptions: Object.values(CountryRegion),
+ globalId: 'unused',
+}
+
+export default {
+ title: 'Features/AdminForm/EditFieldDrawer/EditCountryRegion',
+ component: EditCountryRegion,
+ decorators: [
+ StoryRouter({
+ initialEntries: ['/61540ece3d4a6e50ac0cc6ff'],
+ path: '/:formId',
+ }),
+ EditFieldDrawerDecorator,
+ ],
+ parameters: {
+ // Required so skeleton "animation" does not hide content.
+ chromatic: { pauseAnimationAtEnd: true },
+ msw: createFormBuilderMocks({}, 0),
+ },
+ args: {
+ field: DEFAULT_COUNTRY_REGION_FIELD,
+ },
+} as Meta
+
+interface StoryArgs {
+ field: CountryRegionFieldBase
+}
+
+const Template: Story = ({ field }) => {
+ return
+}
+
+export const Default = Template.bind({})
+Default.storyName = 'EditCountryRegion'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.tsx
new file mode 100644
index 0000000000..76871a0250
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/EditCountryRegion.tsx
@@ -0,0 +1,92 @@
+import { useMemo } from 'react'
+import { FormControl } from '@chakra-ui/react'
+import { extend, pick } from 'lodash'
+
+import { CountryRegionFieldBase } from '~shared/types/field'
+
+import { createBaseValidationRules } from '~utils/fieldValidation'
+import FormErrorMessage from '~components/FormControl/FormErrorMessage'
+import FormLabel from '~components/FormControl/FormLabel'
+import Input from '~components/Input'
+import Textarea from '~components/Textarea'
+import Toggle from '~components/Toggle'
+
+import { CreatePageDrawerContentContainer } from '~features/admin-form/create/common'
+
+import { FormFieldDrawerActions } from '../common/FormFieldDrawerActions'
+import { EditFieldProps } from '../common/types'
+import { useEditFieldForm } from '../common/useEditFieldForm'
+
+type EditCountryRegionProps = EditFieldProps
+
+const EDIT_COUNTRY_FIELD_KEYS = ['title', 'description', 'required'] as const
+
+type EditCountryRegionKeys = typeof EDIT_COUNTRY_FIELD_KEYS[number]
+
+type EditCountryRegionInputs = Pick<
+ CountryRegionFieldBase,
+ EditCountryRegionKeys
+>
+
+const transformCountryRegionFieldToEditForm = (
+ field: CountryRegionFieldBase,
+): EditCountryRegionInputs => {
+ return {
+ ...pick(field, EDIT_COUNTRY_FIELD_KEYS),
+ }
+}
+
+const transformCountryRegionEditFormToField = (
+ inputs: EditCountryRegionInputs,
+ originalField: CountryRegionFieldBase,
+): CountryRegionFieldBase => {
+ return extend({}, originalField, inputs, {})
+}
+
+export const EditCountryRegion = ({
+ field,
+}: EditCountryRegionProps): JSX.Element => {
+ const {
+ register,
+ formState: { errors },
+ buttonText,
+ handleUpdateField,
+ isLoading,
+ handleCancel,
+ } = useEditFieldForm({
+ field,
+ transform: {
+ input: transformCountryRegionFieldToEditForm,
+ output: transformCountryRegionEditFormToField,
+ },
+ })
+
+ const requiredValidationRule = useMemo(
+ () => createBaseValidationRules({ required: true }),
+ [],
+ )
+
+ return (
+
+
+ Question
+
+ {errors?.title?.message}
+
+
+ Description
+
+ {errors?.description?.message}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/index.ts b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/index.ts
new file mode 100644
index 0000000000..b7639e6ab8
--- /dev/null
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/EditCountryRegion/index.ts
@@ -0,0 +1 @@
+export { EditCountryRegion } from './EditCountryRegion'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
index e7056479ad..e88b152019 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/EditFieldDrawer/edit-fieldtype/index.ts
@@ -1,5 +1,6 @@
export * from './EditAttachment'
export * from './EditCheckbox'
+export * from './EditCountryRegion'
export * from './EditDate'
export * from './EditDecimal'
export * from './EditDropdown'
diff --git a/frontend/src/features/admin-form/create/builder-and-design/constants.ts b/frontend/src/features/admin-form/create/builder-and-design/constants.ts
index 82dc078800..2980ad5141 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/constants.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/constants.ts
@@ -19,6 +19,7 @@ export const BASIC_FIELDS_ORDERED = [
BasicField.Radio,
BasicField.Checkbox,
BasicField.Dropdown,
+ BasicField.CountryRegion,
BasicField.Section,
BasicField.Statement,
BasicField.YesNo,
diff --git a/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts b/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
index fb40dfc8b1..3a8d414a8b 100644
--- a/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
+++ b/frontend/src/features/admin-form/create/builder-and-design/utils/fieldCreation.ts
@@ -184,6 +184,13 @@ export const getFieldCreationMeta = (fieldType: BasicField): FieldCreateDto => {
minimumRows: 2,
}
}
+ case BasicField.CountryRegion: {
+ return {
+ fieldType,
+ ...baseMeta,
+ fieldOptions: [],
+ }
+ }
case BasicField.Children: {
return {
fieldType,
diff --git a/frontend/src/features/admin-form/create/constants.ts b/frontend/src/features/admin-form/create/constants.ts
index 93e8864852..9f67303fe3 100644
--- a/frontend/src/features/admin-form/create/constants.ts
+++ b/frontend/src/features/admin-form/create/constants.ts
@@ -107,6 +107,12 @@ export const BASICFIELD_TO_DRAWER_META: {
isSubmitted: true,
},
+ [BasicField.CountryRegion]: {
+ label: 'Country/Region',
+ icon: BiFlag,
+ isSubmitted: true,
+ },
+
[BasicField.Email]: {
label: 'Email',
icon: BiMailSend,
diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx
index 1e0a06d7b1..afb071f892 100644
--- a/frontend/src/features/public-form/PublicFormProvider.tsx
+++ b/frontend/src/features/public-form/PublicFormProvider.tsx
@@ -20,7 +20,7 @@ import {
PAYMENT_CONTACT_FIELD_ID,
PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID,
} from '~shared/constants'
-import { PaymentType } from '~shared/types'
+import { BasicField, PaymentType } from '~shared/types'
import { CaptchaTypes } from '~shared/types/captcha'
import {
FormAuthType,
@@ -306,6 +306,28 @@ export const PublicFormProvider = ({
},
}
+ const countryRegionFieldIds = new Set(
+ form.form_fields
+ .filter((field) => field.fieldType === BasicField.CountryRegion)
+ .map((field) => field._id),
+ )
+ // We want users to see the country/region options in title-case but we also need the data in the backend to remain in upper-case.
+ // Country/region data in the backend needs to remain in upper-case so that they remain consistent with myinfo-countries.
+ const formInputsWithCountryRegionInUpperCase = Object.keys(
+ formInputs,
+ ).reduce((newFormInputs: typeof formInputs, fieldId) => {
+ const currentInput = formInputs[fieldId]
+ if (
+ countryRegionFieldIds.has(fieldId) &&
+ typeof currentInput === 'string'
+ ) {
+ newFormInputs[fieldId] = currentInput.toUpperCase()
+ } else {
+ newFormInputs[fieldId] = currentInput
+ }
+ return newFormInputs
+ }, {})
+
const logMeta = {
action: 'handleSubmitForm',
useFetchForSubmissions,
@@ -339,7 +361,13 @@ export const PublicFormProvider = ({
})
return submitEmailModeFormFetchMutation
- .mutateAsync(formData, { onSuccess })
+ .mutateAsync(
+ {
+ ...formData,
+ formInputs: formInputsWithCountryRegionInUpperCase,
+ },
+ { onSuccess },
+ )
.catch(async (error) => {
datadogLogs.logger.warn(`handleSubmitForm: ${error.message}`, {
meta: {
@@ -371,7 +399,13 @@ export const PublicFormProvider = ({
return (
submitEmailModeFormMutation
- .mutateAsync(formData, { onSuccess })
+ .mutateAsync(
+ {
+ ...formData,
+ formInputs: formInputsWithCountryRegionInUpperCase,
+ },
+ { onSuccess },
+ )
// Using catch since we are using mutateAsync and react-hook-form will continue bubbling this up.
.catch(async (error) => {
// TODO(#5826): Remove when we have resolved the Network Error
@@ -415,6 +449,7 @@ export const PublicFormProvider = ({
.mutateAsync(
{
...formData,
+ formInputs: formInputsWithCountryRegionInUpperCase,
publicKey: form.publicKey,
captchaResponse,
captchaType,
@@ -484,6 +519,7 @@ export const PublicFormProvider = ({
.mutateAsync(
{
...formData,
+ formInputs: formInputsWithCountryRegionInUpperCase,
publicKey: form.publicKey,
captchaResponse,
captchaType,
diff --git a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
index d8de3e8e76..829530d862 100644
--- a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
+++ b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
@@ -7,6 +7,7 @@ import {
AttachmentField,
CheckboxField,
ChildrenCompoundField,
+ CountryRegionField,
DateField,
DecimalField,
DropdownField,
@@ -70,6 +71,8 @@ export const FieldFactory = memo(
return
case BasicField.Dropdown:
return
+ case BasicField.CountryRegion:
+ return
case BasicField.Date:
return
case BasicField.Uen:
diff --git a/frontend/src/features/public-form/utils/createSubmission.ts b/frontend/src/features/public-form/utils/createSubmission.ts
index 17978249fc..693d38290f 100644
--- a/frontend/src/features/public-form/utils/createSubmission.ts
+++ b/frontend/src/features/public-form/utils/createSubmission.ts
@@ -106,7 +106,7 @@ const createResponsesArray = (
): FieldResponse[] => {
const transformedResponses = formFields
.map((ff) => transformInputsToOutputs(ff, formInputs[ff._id]))
- .filter((output): output is FieldResponse => output !== undefined)
+ .filter((output): output is FieldResponse => output !== null)
return validateResponses(transformedResponses)
}
diff --git a/frontend/src/features/public-form/utils/inputTransformation.ts b/frontend/src/features/public-form/utils/inputTransformation.ts
index ffc92b3685..86fb0f201f 100644
--- a/frontend/src/features/public-form/utils/inputTransformation.ts
+++ b/frontend/src/features/public-form/utils/inputTransformation.ts
@@ -199,12 +199,12 @@ const transformToChildOutput = (
* Transforms form inputs to their desire output shapes for sending to the server
* @param field schema to retrieve base field info
* @param input the input corresponding to the field in the form
- * @returns If field type does not need an output, `undefined` is returned. Otherwise returns the transformed output.
+ * @returns If field type does not need an output, `null` is returned. Otherwise returns the transformed output.
*/
export const transformInputsToOutputs = (
field: FormFieldDto,
input: FormFieldValue,
-): FieldResponse | undefined => {
+): FieldResponse | null => {
switch (field.fieldType) {
case BasicField.Section:
return transformToSectionOutput(field)
@@ -250,6 +250,7 @@ export const transformInputsToOutputs = (
case BasicField.LongText:
case BasicField.HomeNo:
case BasicField.Dropdown:
+ case BasicField.CountryRegion:
case BasicField.Rating:
case BasicField.Nric:
case BasicField.Uen:
@@ -260,7 +261,7 @@ export const transformInputsToOutputs = (
case BasicField.Statement:
case BasicField.Image:
// No output needed.
- return undefined
+ return null
case BasicField.Children:
return transformToChildOutput(
field,
diff --git a/frontend/src/features/public-form/utils/validateResponses.ts b/frontend/src/features/public-form/utils/validateResponses.ts
index 0be87d96a1..9443dd5434 100644
--- a/frontend/src/features/public-form/utils/validateResponses.ts
+++ b/frontend/src/features/public-form/utils/validateResponses.ts
@@ -3,6 +3,7 @@ import {
BasicField,
CheckboxResponse,
ChildBirthRecordsResponse,
+ CountryRegionResponse,
DateResponse,
DecimalResponse,
DropdownResponse,
@@ -62,6 +63,8 @@ export const validateResponses = (responses: unknown): FieldResponse[] => {
return DateResponse.parse(response)
case BasicField.Dropdown:
return DropdownResponse.parse(response)
+ case BasicField.CountryRegion:
+ return CountryRegionResponse.parse(response)
case BasicField.Email:
return EmailResponse.parse(response)
case BasicField.HomeNo:
diff --git a/frontend/src/templates/Field/CountryField/CountryField.tsx b/frontend/src/templates/Field/CountryField/CountryField.tsx
deleted file mode 100644
index 2021d78a41..0000000000
--- a/frontend/src/templates/Field/CountryField/CountryField.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Country } from '~shared/constants/countries'
-import {
- BasicField,
- DropdownFieldBase,
- FormFieldWithId,
-} from '~shared/types/field'
-
-import DropdownField from '~templates/Field/Dropdown'
-
-import { BaseFieldProps } from '../FieldContainer'
-
-// TODO: Change to CountryFieldBase when the new CountryField type is added in future PRs
-export type CountryFieldSchema = FormFieldWithId
-export interface CountryFieldProps extends BaseFieldProps {
- schema: CountryFieldSchema
-}
-
-export const CountryField = ({ schema }: CountryFieldProps): JSX.Element => {
- return (
-
- )
-}
diff --git a/frontend/src/templates/Field/CountryField/CountryField.stories.tsx b/frontend/src/templates/Field/CountryRegionField/CountryRegionField.stories.tsx
similarity index 76%
rename from frontend/src/templates/Field/CountryField/CountryField.stories.tsx
rename to frontend/src/templates/Field/CountryRegionField/CountryRegionField.stories.tsx
index 037b865497..ce93d376b1 100644
--- a/frontend/src/templates/Field/CountryField/CountryField.stories.tsx
+++ b/frontend/src/templates/Field/CountryRegionField/CountryRegionField.stories.tsx
@@ -8,24 +8,24 @@ import { BasicField } from '~shared/types/field'
import Button from '~components/Button'
import {
- CountryField as CountryFieldComponent,
- CountryFieldProps,
- CountryFieldSchema,
-} from './CountryField'
+ CountryRegionField as CountryRegionFieldComponent,
+ CountryRegionFieldProps,
+ CountryRegionFieldSchema,
+} from './CountryRegionField'
-const baseSchema: CountryFieldSchema = {
- title: 'Country',
- description: 'Type or select your residential country',
+const baseSchema: CountryRegionFieldSchema = {
+ title: 'Country/Region',
+ description: 'Type or select your residential country/region',
required: true,
disabled: false,
- fieldType: BasicField.Dropdown,
+ fieldType: BasicField.CountryRegion,
fieldOptions: [],
_id: 'random-id',
}
export default {
- title: 'Templates/Field/CountryField',
- component: CountryFieldComponent,
+ title: 'Templates/Field/CountryRegionField',
+ component: CountryRegionFieldComponent,
decorators: [],
parameters: {
docs: {
@@ -42,11 +42,14 @@ export default {
},
} as Meta
-interface StoryCountryFieldProps extends CountryFieldProps {
+interface StoryCountryRegionFieldProps extends CountryRegionFieldProps {
defaultValue?: string
}
-const Template: Story = ({ defaultValue, ...args }) => {
+const Template: Story = ({
+ defaultValue,
+ ...args
+}) => {
const formMethods = useForm({
defaultValues: {
[args.schema._id]: defaultValue,
@@ -70,7 +73,7 @@ const Template: Story = ({ defaultValue, ...args }) => {
return (