Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save email into GoogleSpreadsheet #125

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions signer-service/src/api/controllers/email.controller.js
Original file line number Diff line number Diff line change
@@ -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)
26 changes: 26 additions & 0 deletions signer-service/src/api/controllers/googleSpreadSheet.controller.js
Original file line number Diff line number Diff line change
@@ -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 });
}
};
28 changes: 4 additions & 24 deletions signer-service/src/api/controllers/storage.controller.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 });
}
};
17 changes: 11 additions & 6 deletions signer-service/src/api/middlewares/validators.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -33,19 +34,22 @@ 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 });
}

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;

Expand Down Expand Up @@ -97,4 +101,5 @@ module.exports = {
validatePreSwapSubsidizationInput,
validatePostSwapSubsidizationInput,
validateStorageInput,
validateEmailInput,
};
9 changes: 9 additions & 0 deletions signer-service/src/api/routes/v1/email.route.js
Original file line number Diff line number Diff line change
@@ -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;
15 changes: 15 additions & 0 deletions signer-service/src/api/routes/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -33,13 +34,27 @@ router.get('/status', sendStatusWithPk);
* POST v1/stellar
*/
router.use('/stellar', stellarRoutes);


/**
* POST v1/pendulum
*/
router.use('/pendulum', pendulumRoutes);

/**
* POST v1/storage
*/
router.use('/storage', storageRoutes);

/**
* POST v1/email
*/
router.use('/email', emailRoutes);
Sharqiewicz marked this conversation as resolved.
Show resolved Hide resolved


/**
* POST v1/subsidize
*/
router.use('/subsidize', subsidizeRoutes);

module.exports = router;
3 changes: 2 additions & 1 deletion signer-service/src/config/vars.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
66 changes: 59 additions & 7 deletions src/components/EmailForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center justify-center mt-2 text-white btn-success btn">Successfully saved!</div>
);
}

if (isPending) {
return <div className="flex items-center justify-center mt-2 text-white bg-blue-700 btn">Loading...</div>;
}

return (
<>
<div className="flex items-center justify-center mt-2">
<div className="mr-3 grow">
<TextInput type="email" placeholder="example@mail.com" register={register('email')} />
</div>
<button className="px-5 text-white bg-blue-700 btn rounded-xl" type="submit">
Submit
</button>
</div>
{isError && (
<p className="mt-1 text-center text-red-600" id="request-error-message">
Error while saving your email. Please try again.
</p>
)}
</>
);
};

return (
<section className="w-full">
<form className="w-full" onSubmit={onSubmit} aria-errormessage={isError ? 'request-error-message' : undefined}>
<p className="font-light text-center text-blue-700">
To receive further assistance and information about our app,
</p>
<p className="font-light text-center text-blue-700">please provide your email address below:</p>
<div className="w-full mt-2">
<TextInput type="email" placeholder="example@mail.com" register={register('email')} />
</div>
</section>
<FormButtonSection />
</form>
);
};
15 changes: 14 additions & 1 deletion src/components/TextInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { Input } from 'react-daisyui';
import { UseFormRegisterReturn } from 'react-hook-form';

const patterns: Record<string, string> = {
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;
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/failure/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const FailurePage = ({ finishOfframping, transactionId }: FailurePageProp
<div className="h-0.5 m-auto w-1/5 bg-pink-500 mt-8 mb-5" />
<p className="text-center text-gray-400">If you continue to experience issues, contact support on:</p>
<TelegramButton />
<EmailForm />
<EmailForm transactionId={transactionId} />
<button className="w-full mt-5 text-white bg-blue-700 btn rounded-xl" onClick={finishOfframping}>
Try again
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/success/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const SuccessPage = ({ finishOfframping, transactionId }: SuccessPageProp
If your transaction is not completed after 60 minutes please contact support on:
</p>
<TelegramButton />
<EmailForm />
<EmailForm transactionId={transactionId} />
<button className="w-full mt-5 text-white bg-blue-700 btn rounded-xl" onClick={finishOfframping}>
Return Home
</button>
Expand Down
28 changes: 22 additions & 6 deletions src/services/storage/remote.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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',
Expand All @@ -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);
}
Loading