diff --git a/signer-service/src/api/controllers/email.controller.js b/signer-service/src/api/controllers/email.controller.js new file mode 100644 index 00000000..3d0dd6d6 --- /dev/null +++ b/signer-service/src/api/controllers/email.controller.js @@ -0,0 +1,13 @@ +const { spreadsheet } = require('../../config/vars'); +const { storeDataInGoogleSpreadsheet } = require('./googleSpreadsheet.controller'); + +// These are the headers for the Google Spreadsheet +const EMAIL_SHEET_HEADER_VALUES = [ + 'timestamp', + 'email', + 'transactionId' +]; + +exports.EMAIL_SHEET_HEADER_VALUES = EMAIL_SHEET_HEADER_VALUES; + +exports.storeEmail = async (req, res) => storeDataInGoogleSpreadsheet(req, res, spreadsheet.emailSheetId, EMAIL_SHEET_HEADER_VALUES) diff --git a/signer-service/src/api/controllers/googleSpreadSheet.controller.js b/signer-service/src/api/controllers/googleSpreadSheet.controller.js new file mode 100644 index 00000000..520a990e --- /dev/null +++ b/signer-service/src/api/controllers/googleSpreadSheet.controller.js @@ -0,0 +1,26 @@ +require('dotenv').config(); + +const { spreadsheet } = require('../../config/vars'); +const { initGoogleSpreadsheet, getOrCreateSheet, appendData } = require('../services/spreadsheet.service'); + + +exports.storeDataInGoogleSpreadsheet = async (req, res, spreadsheetId, sheetHeaderValues) => { + try { + // We expect the data to be an object that matches our schema + const data = req.body; + + // Try dumping transactions to spreadsheet + const sheet = await initGoogleSpreadsheet(spreadsheetId, spreadsheet.googleCredentials).then((doc) => getOrCreateSheet(doc, sheetHeaderValues)); + + if (sheet) { + console.log('Appending data to sheet'); + await appendData(sheet, data); + return res.status(200).json({ message: 'Data stored successfully' }); + } + + return res.status(500).json({ error: 'Failed to store data. Sheet unavailable.' }); + } catch (error) { + console.error('Error in storeData:', error); + return res.status(500).json({ error: 'Failed to store data', details: error.message }); + } +}; diff --git a/signer-service/src/api/controllers/storage.controller.js b/signer-service/src/api/controllers/storage.controller.js index f94b0bea..c859dbf1 100644 --- a/signer-service/src/api/controllers/storage.controller.js +++ b/signer-service/src/api/controllers/storage.controller.js @@ -1,10 +1,8 @@ -require('dotenv').config(); - const { spreadsheet } = require('../../config/vars'); -const { initGoogleSpreadsheet, getOrCreateSheet, appendData } = require('../services/spreadsheet.service'); +const { storeDataInGoogleSpreadsheet } = require('./googleSpreadsheet.controller'); // These are the headers for the Google Spreadsheet -exports.SHEET_HEADER_VALUES = [ +const DUMP_SHEET_HEADER_VALUES = [ 'timestamp', 'polygonAddress', 'stellarEphemeralPublicKey', @@ -16,25 +14,7 @@ exports.SHEET_HEADER_VALUES = [ 'stellarCleanupTx', ]; -exports.storeData = async (req, res, next) => { - try { - // We expect the data to be an object that matches our schema - const data = req.body; - - // Try dumping transactions to spreadsheet - const sheet = await initGoogleSpreadsheet(spreadsheet.sheetId, spreadsheet.googleCredentials).then((doc) => { - return getOrCreateSheet(doc, this.SHEET_HEADER_VALUES); - }); +exports.DUMP_SHEET_HEADER_VALUES = DUMP_SHEET_HEADER_VALUES; - if (sheet) { - console.log('Appending data to sheet'); - await appendData(sheet, data); - return res.status(200).json({ message: 'Data stored successfully' }); - } +exports.storeData = async (req, res) => storeDataInGoogleSpreadsheet(req, res, spreadsheet.storageSheetId, DUMP_SHEET_HEADER_VALUES) - return res.status(500).json({ error: 'Failed to store data. Sheet unavailable.', details: error.message }); - } catch (error) { - console.error('Error in storeData:', error); - return res.status(500).json({ error: 'Failed to store data', details: error.message }); - } -}; diff --git a/signer-service/src/api/middlewares/validators.js b/signer-service/src/api/middlewares/validators.js index a514d0be..ee5dfdba 100644 --- a/signer-service/src/api/middlewares/validators.js +++ b/signer-service/src/api/middlewares/validators.js @@ -1,5 +1,6 @@ -const { SHEET_HEADER_VALUES } = require('../controllers/storage.controller'); const { TOKEN_CONFIG } = require('../../constants/tokenConfig'); +const { DUMP_SHEET_HEADER_VALUES } = require('../controllers/storage.controller'); +const { EMAIL_SHEET_HEADER_VALUES } = require('../controllers/email.controller'); const validateCreationInput = (req, res, next) => { const { accountId, maxTime, assetCode } = req.body; @@ -33,12 +34,12 @@ const validateChangeOpInput = (req, res, next) => { next(); }; -const validateStorageInput = (req, res, next) => { +const validateInputHeaderValues = (requiredHeaders) => (req, res, next) => { const data = req.body; - // Check if the data contains values for all the headers - if (!SHEET_HEADER_VALUES.every((header) => data[header])) { - const missingItems = SHEET_HEADER_VALUES.filter((header) => !data[header]); - let errorMessage = 'Data does not match schema. Missing items: ' + missingItems.join(', '); + + if (!requiredHeaders.every((header) => data[header])) { + const missingItems = requiredHeaders.filter((header) => !data[header]); + const errorMessage = 'Data does not match schema. Missing items: ' + missingItems.join(', '); console.error(errorMessage); return res.status(400).json({ error: errorMessage }); } @@ -46,6 +47,9 @@ const validateStorageInput = (req, res, next) => { next(); }; +const validateStorageInput = validateInputHeaderValues(DUMP_SHEET_HEADER_VALUES); +const validateEmailInput = validateInputHeaderValues(EMAIL_SHEET_HEADER_VALUES); + const validatePreSwapSubsidizationInput = (req, res, next) => { const { amountRaw, address } = req.body; @@ -97,4 +101,5 @@ module.exports = { validatePreSwapSubsidizationInput, validatePostSwapSubsidizationInput, validateStorageInput, + validateEmailInput, }; diff --git a/signer-service/src/api/routes/v1/email.route.js b/signer-service/src/api/routes/v1/email.route.js new file mode 100644 index 00000000..806eae22 --- /dev/null +++ b/signer-service/src/api/routes/v1/email.route.js @@ -0,0 +1,9 @@ +const express = require('express'); +const controller = require('../../controllers/email.controller'); +const { validateEmailInput } = require('../../middlewares/validators'); + +const router = express.Router({ mergeParams: true }); + +router.route('/create').post(validateEmailInput, controller.storeEmail); + +module.exports = router; diff --git a/signer-service/src/api/routes/v1/index.js b/signer-service/src/api/routes/v1/index.js index eb82741b..67dfe5f6 100644 --- a/signer-service/src/api/routes/v1/index.js +++ b/signer-service/src/api/routes/v1/index.js @@ -3,6 +3,7 @@ const express = require('express'); const stellarRoutes = require('./stellar.route'); const pendulumRoutes = require('./pendulum.route'); const storageRoutes = require('./storage.route'); +const emailRoutes = require('./email.route'); const subsidizeRoutes = require('./subsidize.route'); const router = express.Router({ mergeParams: true }); @@ -33,6 +34,11 @@ router.get('/status', sendStatusWithPk); * POST v1/stellar */ router.use('/stellar', stellarRoutes); + + +/** + * POST v1/pendulum + */ router.use('/pendulum', pendulumRoutes); /** @@ -40,6 +46,15 @@ router.use('/pendulum', pendulumRoutes); */ router.use('/storage', storageRoutes); +/** + * POST v1/email + */ +router.use('/email', emailRoutes); + + +/** + * POST v1/subsidize + */ router.use('/subsidize', subsidizeRoutes); module.exports = router; diff --git a/signer-service/src/config/vars.js b/signer-service/src/config/vars.js index 3aa64104..ec17e533 100644 --- a/signer-service/src/config/vars.js +++ b/signer-service/src/config/vars.js @@ -19,6 +19,7 @@ module.exports = { email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, key: process.env.GOOGLE_PRIVATE_KEY?.split(String.raw`\n`).join('\n'), }, - sheetId: process.env.GOOGLE_SPREADSHEET_ID, + storageSheetId: process.env.GOOGLE_SPREADSHEET_ID, + emailSheetId: process.env.GOOGLE_EMAIL_SPREADSHEET_ID, }, }; diff --git a/src/components/EmailForm/index.tsx b/src/components/EmailForm/index.tsx index 3fc81ef6..06c6a4d9 100644 --- a/src/components/EmailForm/index.tsx +++ b/src/components/EmailForm/index.tsx @@ -1,18 +1,70 @@ +import { useMutation } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; +import { storeUserEmailInBackend } from '../../services/storage/remote'; import { TextInput } from '../../components/TextInput'; -export const EmailForm = () => { - const { register } = useForm(); +interface EmailFormProps { + transactionId?: string; +} + +export const EmailForm = ({ transactionId }: EmailFormProps) => { + const { register, handleSubmit } = useForm(); + + const { + mutate: saveUserEmailMutation, + isPending, + isSuccess, + isError, + } = useMutation({ + mutationFn: storeUserEmailInBackend, + }); + + const onSubmit = handleSubmit((data) => { + if (!transactionId) { + console.error('Transaction ID is missing'); + return; + } + + saveUserEmailMutation({ email: data.email, transactionId }); + }); + + const FormButtonSection = () => { + if (isSuccess) { + return ( +
Successfully saved!
+ ); + } + + if (isPending) { + return
Loading...
; + } + + return ( + <> +
+
+ +
+ +
+ {isError && ( +

+ Error while saving your email. Please try again. +

+ )} + + ); + }; return ( -
+

To receive further assistance and information about our app,

please provide your email address below:

-
- -
-
+ + ); }; diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 58842041..ff796832 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,6 +1,19 @@ import { Input } from 'react-daisyui'; import { UseFormRegisterReturn } from 'react-hook-form'; +const patterns: Record = { + email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', + default: '^(0x[a-fA-F0-9]{40})$', +}; + +function getPattern(textInputType?: string) { + if (textInputType && patterns[textInputType]) { + return patterns[textInputType]; + } + + return patterns.default; +} + interface NumericInputProps { register: UseFormRegisterReturn; readOnly?: boolean; @@ -35,7 +48,7 @@ export const TextInput = ({ spellCheck="false" placeholder={placeholder} error={error} - pattern="^(0x[a-fA-F0-9]{40})$" + pattern={getPattern(type)} readOnly={readOnly} disabled={disabled} autoFocus={autoFocus} diff --git a/src/pages/failure/index.tsx b/src/pages/failure/index.tsx index 0c6bc266..4eb07043 100644 --- a/src/pages/failure/index.tsx +++ b/src/pages/failure/index.tsx @@ -30,7 +30,7 @@ export const FailurePage = ({ finishOfframping, transactionId }: FailurePageProp

If you continue to experience issues, contact support on:

- + diff --git a/src/pages/success/index.tsx b/src/pages/success/index.tsx index e1d9298b..ba761923 100644 --- a/src/pages/success/index.tsx +++ b/src/pages/success/index.tsx @@ -29,7 +29,7 @@ export const SuccessPage = ({ finishOfframping, transactionId }: SuccessPageProp If your transaction is not completed after 60 minutes please contact support on:

- + diff --git a/src/services/storage/remote.ts b/src/services/storage/remote.ts index 39305b8a..4aa7aecb 100644 --- a/src/services/storage/remote.ts +++ b/src/services/storage/remote.ts @@ -1,7 +1,7 @@ import { SIGNING_SERVICE_URL } from '../../constants/constants'; // These are the headers for the Google Spreadsheet -type Data = { +interface DumpData { timestamp: string; polygonAddress: string; stellarEphemeralPublicKey: string; @@ -11,10 +11,15 @@ type Data = { spacewalkRedeemTx: string; stellarOfframpTx: string; stellarCleanupTx: string; -}; +} + +interface EmailData { + email: string; + transactionId: string; +} -export async function storeDataInBackend(data: Data) { - const response = await fetch(`${SIGNING_SERVICE_URL}/v1/storage/create`, { +async function sendRequestToBackend(endpoint: string, data: EmailData | DumpData) { + const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -23,8 +28,19 @@ export async function storeDataInBackend(data: Data) { }); if (!response.ok) { - throw new Error(`Error while sending data to storage endpoint`); + throw new Error(`Error while sending data to ${endpoint}`); } - console.log('Data stored successfully'); + return await response.json(); +} + +export async function storeDataInBackend(data: DumpData) { + const endpoint = `${SIGNING_SERVICE_URL}/v1/storage/create`; + return await sendRequestToBackend(endpoint, data); +} + +export async function storeUserEmailInBackend(data: EmailData) { + const endpoint = `${SIGNING_SERVICE_URL}/v1/email/create`; + const payload = { ...data, timestamp: new Date().toISOString() }; + return await sendRequestToBackend(endpoint, payload); }