How to delete OTP user input on Backspace key press - javascript

I've coded a OTP component but it has two issues that I couldn't solve.
The first issue is, when I press Backspace I want to delete the input value and then go to the previous input field to delete it.
The second issue is, when I delete a input (i.e the 3rd box) my code makes me focus on the next input field.
How can I implement Backspace key press properly and go to the previous input field when a input is deleted?
Here's what I've done so far:
import React, { useState } from "react";
import css from '../components/css/OTP.css'
const Axios = require('axios')
const OTP = () => {
const [otp, setOtp] = useState(new Array(4).fill(""));
const handleChange = (element, index) => {
if (isNaN(element.value)) return false;
setOtp([...otp.map((d, idx) => (idx === index ? element.value : d))]);
//Focus next input
if (element.nextSibling) {
element.nextSibling.focus();
}
}
//eğer girilen OTP backendden gelen OTP ile aynıysa matchleştiğini göster ve kullanıcıyı verifike et daha sonra dashboarda aktar.
const checkOTP = async () =>{
try
{
const url = "http://localhost:5000/auth/otp"
const response = await Axios.post(url , {otp} , {withCredentials: true})
if(response.status!==200){
alert("Invalid OTP entry")
}
else{
alert("OTP successful!")
}
}
catch(err)
{
console.log(err)
}
}
return (
<>
<div className="otp-window">
<div className="otp-modal">
<div className="row">
<div className="info">
<p className="otp-info">Please enter the OTP sent to your email</p>
{otp.map((data, index) => {
return (
<input
className="otp-field"
type="text"
name="otp"
maxLength="1"
key={index}
value={data}
onChange={e => handleChange(e.target, index)}
onFocus={e => e.target.select()}
/>
);
})}
<p className="otp-entered">OTP : {otp.join("")}</p>
<p>
<button
className="clear-button"
onClick={e => setOtp([...otp.map(v => "")])}
>
Clear
</button>
<button
className="verify-button"
onClick={e =>
alert("Entered OTP is " + otp.join(""))
}
onClick={checkOTP}>
Verify OTP
</button>
</p>
</div>
</div>
</div>
</div>
</>
);
};
export default OTP;

You can use an input event and check the event.inputType property of the event object like this. You might have to change the onChange prop to onInput though.
if (event.inputType === 'deleteContentBackward' && event.target.value === '') {
// Focus on the previous field
}

Related

Why isn't my child component updating data when changing the state in React?

I have a list of users and I want to display in another component on the same page the user data in input fields for every user that I click on.
When no user is selected, I want the component to just render some text and a button to add a user. When the button is clicked the component renders the form with empty input fields so that we can add a new user.
I tried the following, but it's just showing the data for the first one I click on. It's not updating.
The main page:
const index = (props) => {
const [selectedUser, setSelectedUser] = useState(null);
const [state, setState] = useState("Index");
const onChange = (item) => {
setState("Edit");
setSelectedUser(item);
};
const onClick = (e, item) => {
if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
onChange(item);
} else {
console.log('prevented "onClick" on keypress');
}
};
const renderComponent = () => {
switch (state) {
case "Index":
return (
<>
<div className="btn" onClick={(e) => setState("Edit")}>
+ New Staff
</div>
<img src="/storage/illustrations/collaboration.svg" />
</>
);
case "Edit":
return (
<div>
<StaffForm profile={selectedUser} />
</div>
);
}
};
return (
<>
<div>
<div>
<h1>Staff</h1>
</div>
<div>
<div>
{profiles.map((item, index) => {
return (
<div key={index} onClick={(e) => onClick(e, item)}>
<input
type={"radio"}
name={"staff"}
checked={state === item}
onChange={(e) => onChange(item)}
/>
<span>{item.user.name}</span>
</div>
);
})}
</div>
<div>{renderComponent()}</div>
</div>
</div>
</>
);
};
The Staff Form Component:
const StaffForm = ({ profile }) => {
const { data, setData, post, processing, errors, reset } = useForm({
email: profile ? profile.user.email : "",
name: profile ? profile.user.name : "",
phone_number: profile ? profile.user.phone_number : "",
avatar: profile ? profile.user.avatar : "",
});
const [file, setFile] = useState(data.avatar);
const handleImageUpload = (e) => {
setFile(URL.createObjectURL(e.target.files[0]));
setData("avatar", e.target.files[0]);
};
const onHandleChange = (event) => {
setData(
event.target.name,
event.target.type === "checkbox"
? event.target.checked
: event.target.value
);
};
return (
<div>
<ImageUpload
name={data.name}
file={file}
handleImageUpload={handleImageUpload}
/>
<TextInput
type="text"
name="name"
value={data.name}
autoComplete="name"
isFocused={true}
onChange={onHandleChange}
placeholder={t("Name")}
required
/>
<TextInput
type="text"
name="phone_number"
value={data.phone_number}
autoComplete="phone_number"
placeholder={t("Phone Number")}
onChange={onHandleChange}
required
/>
<TextInput
type="email"
name="email"
value={data.email}
autoComplete="email"
onChange={onHandleChange}
placeholder={t("Email")}
required
/>
</div>
);
};
First of all something you should avoid is the renderComponent() call.Check here the first mistake mentioned in this video. This will most likely fix your problem but even if it doesn't the video explains why it should not be used.
Something else that caught my eye(possibly unrelated to your question but good to know) is the onChange function. When two pieces of state change together it is a potential source of problems, check out this article on when to use the useReducer hook.
Also be careful with naming React Components, they need to be capital case, this question contains appropriate answers explaining it.
(To only solve your problem stick to no.1 of this list, there are some improvements i'd do here overall for code quality and beauty, msg me for more details)

React: [Label]-[input]-[edit btn]-[save btn-(hidden)]

As the title states, Ive got a simple crud operation I could use some help with the useEffect/useState hook in how to implement.
I've got the input set to disabled by default, displaying the previous value via the placeholder. The state is then updated via a useState/useEffect hooks. Also, the second button (save) is set to hidden by default.
Goal:
essentially, just trying to setup an event listener for each edit-input button: hide the edit button, enable the input, unhide the save button.
with a separate (2nd) event listener on the save button: to hide the save button, unhide the edit button, return the input to disabled, turn the placeholder to the new value, and submit the value(I've got a good idea of the last part)
JSX:
<label className="col">
<div className="row">
<p className="bold nowrap inline-value-title" >Test Name: </p>
<input
id="display-name-change"
type="text"
className="form-reset inline-input"
onChange={(e) => setDisplayName(e.target.value)}
value={displayName}
placeholder={user.displayName}
disabled
/>
<button type="button" className="inline-button edit-val-btn">
<FontAwesomeIcon icon={faPenToSquare} />
</button>
<button type="button" className="hidden inline-button save-val-btn">.
<FontAwesomeIcon icon={faCircleCheck} />
</button>
</div>
</label>
My Javascript: (as you can probably tell it's still very Vanilla and I think that's the problem)...
const editValueBtns = document.querySelectorAll('.edit-val-btn');
const saveValueBtns = document.querySelectorAll('.save-val-btn');
useEffect(() => {
editValueBtns.forEach((button) => {
button.addEventListener('click', (e) => {
button.classList.add('hidden')
button.nextSibling.classList.remove('hidden')
button.parentElement.children[1].removeAttr("disabled") ;
})
})
saveValueBtns.forEach((button) => {
button.addEventListener('click', (e) => {
button.classList.add('hidden')
button.previousSibling.classList.remove('hidden')
button.parentElement.children[1].addAttr("disabled") ;
})
})
}, []);
EDIT: Showing the inputs being submitted to Firebase/Firestore
const handleSubmit = async (e) => {
e.preventDefault();
let selectedFile = document.querySelector('#thumbnailInput')
// displayname
if(displayName.length == 0){console.log('No change of name')}
else {
console.log('change to displayname')
updateProfile(user, { displayName })
setDoc(doc(db, 'users', user.uid), { displayName }, { merge: true })
}
// phone Number
if (phoneNo.length == 0){console.log('No change of phone no')}
else {
console.log('change to phone')
updateProfile(user, { phoneNo })
setDoc(doc(db, 'users', user.uid), { phoneNo }, { merge: true })
}
// title
if (title.length == 0){console.log('No change of title')}
else {
console.log('change to title')
updateProfile(user, { title })
setDoc(doc(db, 'users', user.uid), { title }, { merge: true })
}
// avatar thumbnail
if(selectedFile.files[0] == undefined){
console.log('no change to thumbnail')
} else {
console.log('change to thumbnail')
// pass the path in ref to create a StorageReference
const storageRef = ref(storage,`thumbnails/${user.uid}/${displayName}`) //thumbnail.name
// upload image, file is a blob here
await uploadBytes(storageRef, thumbnail);
const downloadUrl = await getDownloadURL(storageRef);
// this function returns promise too, add await
await updateProfile(user, { photoURL: downloadUrl })
updateProfile(user, { photoURL: downloadUrl})
setDoc(doc(db, 'users', user.uid), {
photoURL: downloadUrl,
}, { merge: true })
}
// clear all form inputs
const inputs = e.target.querySelectorAll('.form-reset')
inputs.forEach((input) => {
input.value=""
})
}
I see your vanilla js way, and raise you the react way. In react, you shouldn't have to use document.querySelector, previousSibling, parentElement, classList.add, classList.remove, addAttr or button.addEventListener. See solution in CodeSandbox or below:
App.jsx
import { Row } from "./components/Row";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Row placeholder="input 1" />
<Row placeholder="input 2" />
<Row placeholder="input 3" />
</div>
);
}
Row.jsx
import { useState } from "react";
export const Row = ({ defaultValue, placeholder }) => {
const [value, setValue] = useState(defaultValue);
const [disabled, setDisabled] = useState(true);
const handleEditClick = () => {
setDisabled(false);
};
const handleSaveClick = () => {
setDisabled(true);
// save logic goes here
};
return (
<label className="col">
<div className="row">
<p className="bold nowrap inline-value-title">Test Name:</p>
<input
type="text"
className="form-reset inline-input"
onChange={(e) => {
setValue(e.target.value);
}}
value={value}
placeholder={placeholder}
disabled={disabled}
/>
{disabled && (
<button
type="button"
onClick={handleEditClick}
className="inline-button edit-val-btn"
>
edit
</button>
)}
{!disabled && (
<button
type="button"
onClick={handleSaveClick}
className="hidden inline-button save-val-btn"
>
save
</button>
)}
</div>
</label>
);
};
small tweaks.. it appears that logging the console kept it from doing what it did. also removed the e from the parenthesis after each 'click'. the addAttr and removeAttr also were replaced... the rest of the functionality can be placed in either of the listeners..
EDIT: added a 2nd input and it appears to only work in the 1st input....
...yes I'm talking to myself.
EDIT 2: It worked fine until the page refreshed... removed the dependency array for it to work every time. I feel like i still need a cleanup function, but I can't just place the event listeners into a function can i? Really, if you're reading this i would love some more experienced input... :)
useEffect(() => {
editValueBtns.forEach((button) => {
button.addEventListener('click', () => {
button.classList.add('hidden')
button.nextSibling.classList.remove('hidden')
button.parentElement.children[1].disabled = false ;
})
})
saveValueBtns.forEach((button) => {
button.addEventListener('click', () => {
button.classList.add('hidden')
button.previousSibling.classList.remove('hidden')
button.parentElement.children[1].disabled = true ;
})
})
});

How to get the value of ckeckbox element from a form submission in React.js?

For some reason when I submit a form I can't get the value of the checked boxes I don't mean the check value but the value attribute of the HTML element itself.
This is my element in the parent component :
function parent(props){
const [filterCriteria, setFilterCriteria] = useState([]);
const [companies, setCompanies] = useState([]);
const [categories, setCategories] = useState([]);
useEffect(() => {
axios
.get("http://localhost:4000/api/companies")
.then((res) => {
companiesData = res.data.companies;
setCompanies([...new Set(companiesData.map((item) => item.company))]);
})
.finally(() => {
setIslLoading(false);
});
}, []);
useEffect(() => {
axios
.get("http://localhost:4000/api/categories")
.then((res) => {
categoriesData = res.data.categories;
setCategories([
...new Set(categoriesData.map((item) => item.category)),
]);
})
.finally(() => {
setIslLoading(false);
});
}, []);
const onFilteringHandler = (e) => {
e.preventDefault();
//fun fact the e.target is looped useing "for of" not "for in" although it's an
object
for (const element of e.target) {
if (element.type === "checkbox" &&
element.checked === true &&
companies.includes(element.value)) {
setFilterCriteria([...filterCriteria, element.value]);
}
if (element.type === "checkbox" &&
element.checked === true &&
categories.includes(element.value)) {
setFilterCriteria([...filterCriteria, element.value]);
}
}
};
return (
//this element is in the medal of tother elements but for simplification i
removed them
<FilterBox
categoriesCriteria={categories}
settingFilterCriteria={onFilteringHandler}
companiesCriteria={companies}
/>
)
}
This is my element in the child "FilterBox" component :
function FilterBox(props) {
// this is to map the categories into check boxes with values, keys and label
let categoriesFilters = props.categoriesCriteria.map((category, index) => {
return (
<label key={index}>
{category}
//this is the value i'm tring to get in the form submition
<input type="checkbox" value={category} />
</label>
);
});
// this is to map the categories into check boxes with values, keys and label
let companiesFilters = props.companiesCriteria.map((company, index) => {
return (
<label key={index}>
{company}
//this is the value i'm tring to get in the form submition
<input type="checkbox" value={company} />
</label>
);
});
return (
<form
onSubmit={props.settingFilterCriteria}
className="product_creation_form"
>
<div>
<h5 className="filter_title">Categories</h5>
{categoriesFilters}
</div>
<div>
<h5 className="filter_title">Companies</h5>
{companiesFilters}
</div>
<button type="submit" className="form_button">
Apply
</button>
<button type="reset" className="form_button">
Clear All
</button>
</form>
);
}
Nowhere is the issue, I can't get the value in this line:
setFilterCriteria([...filterCriteria, element.value]);
Although the element.value exist on the e.target?
How to do that?
FilterBox Component
function FilterBox(props) {
const categoriesFilters = props.categoriesCriteria.map((category, index) => {
return (
<label key={index}>
{category}
<input type="checkbox" value={category} name={category} />
</label>
);
});
const companiesFilters = props.companiesCriteria.map((company, index) => {
return (
<label key={index}>
{company}
<input type="checkbox" value={company} name={company} />
</label>
);
});
return (
<form
onSubmit={props.settingFilterCriteria}
className="product_creation_form"
>
<div>
<h5 className="filter_title">Categories</h5>
{categoriesFilters}
</div>
<div>
<h5 className="filter_title">Companies</h5>
{companiesFilters}
</div>
<button type="submit" className="form_button">
Apply
</button>
<button type="reset" className="form_button">
Clear All
</button>
</form>
);
}
App Component:
function App() {
const [companies] = React.useState(["companyOne", "companyTwo"]);
const [categories] = React.useState(["categoryOne", "categoryTwo"]);
const onFilteringHandler = (e) => {
e.preventDefault();
const filterCriteria = { companies: [], categories: [] };
for (const element of e.target) {
if (element.type === "checkbox" && element.checked) {
if (companies.includes(element.value)) {
filterCriteria.companies.push(element.name);
}
if (categories.includes(element.value)) {
filterCriteria.categories.push(element.name);
}
}
}
console.log(filterCriteria);
};
return (
<FilterBox
categoriesCriteria={categories}
settingFilterCriteria={onFilteringHandler}
companiesCriteria={companies}
/>
);
}
I took the liberty to disconnect the initial API loading of the companies & categories react state arrays and hardcoded them for simplicity. The below code should be enough to send meaningful data to be able to filter out on the backend.
I took the approach with a non-controlled form state, as your code seemed to favour this kind of approach. You can try and keep the filter values in the react state too but I believe this complicates the solution a bit (especially that state update is async) and it seems that you only need to collect the current filters state value on a button press. If however, you need it for some other purposes, then a controlled form state would most likely be needed.

Formik - State and initial values gets overwritten

I want to implement an edit dialog for some metadata that I get from the backend. For this, I'm using Formik. When the user changes one metadata field then an icon is shown, which indicates that the field was changed. On submit, only the updated values should be sent to the backend. I read in other posts that you should compare the current field values with the initial values provided to the Formik form. This works perfectly fine for single values, like a changed title. However, the form I need to implement has also multi-value fields like creators. I implemented a custom field for that where the user can choose/provide multiple values for one field. The values of this field are saved as an array in the Formik form. The problem is that Formik also changes the initialValue of this field to the currently saved array and therefore I'm no longer able to check if the field is updated or not. Furthermore, I save the metadata fields provided by the backend in a state value because the response contains some further information needed for further implementation. This state value also contains the current value (before update) that a metadata field has and is used as initial values for the Formik form. Strangely, the multi-field component not only overwrites the initialValue of the field of the Formik form but also the value of the field in the state which is only read and never directly updated.
My dialog for editing metadata looks as follows:
const EditMetadataEventsModal = ({ close, selectedRows, updateBulkMetadata }) => {
const { t } = useTranslation();
const [selectedEvents, setSelectedEvents] = useState(selectedRows);
const [metadataFields, setMetadataFields] = useState({});
const [fetchedValues, setFetchedValues] = useState(null);
useEffect(() => {
async function fetchData() {
let eventIds = [];
selectedEvents.forEach((event) => eventIds.push(event.id));
// metadata for chosen events is fetched from backend and saved in state
const responseMetadataFields = await fetchEditMetadata(eventIds);
let initialValues = getInitialValues(responseMetadataFields);
setFetchedValues(initialValues);
setMetadataFields(responseMetadataFields);
}
fetchData();
}, []);
const handleSubmit = (values) => {
const response = updateBulkMetadata(metadataFields, values);
close();
};
return (
<>
<div className="modal-animation modal-overlay" />
<section className="modal wizard modal-animation">
<header>
<a className="fa fa-times close-modal" onClick={() => close()} />
<h2>{t('BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION')}</h2>
</header>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={currentLanguage.dateLocale}>
<Formik initialValues={fetchedValues} onSubmit={(values) => handleSubmit(values)}>
{(formik) => (
<>
<div className="modal-content">
<div className="modal-body">
<div className="full-col">
<div className="obj header-description">
<span>{t('EDIT.DESCRIPTION')}</span>
</div>
<div className="obj tbl-details">
<header>
<span>{t('EDIT.TABLE.CAPTION')}</span>
</header>
<div className="obj-container">
<table className="main-tbl">
<tbody>
{metadataFields.mergedMetadata.map(
(metadata, key) =>
!metadata.readOnly && (
<tr key={key} className={cn({ info: metadata.differentValues })}>
<td>
<span>{t(metadata.label)}</span>
{metadata.required && <i className="required">*</i>}
</td>
<td className="editable ng-isolated-scope">
{/* Render single value or multi value input */}
{console.log('field value')}
{console.log(fetchedValues[metadata.id])}
{metadata.type === 'mixed_text' &&
!!metadata.collection &&
metadata.collection.length !== 0 ? (
<Field name={metadata.id} fieldInfo={metadata} component={RenderMultiField} />
) : (
<Field
name={metadata.id}
metadataField={metadata}
showCheck
component={RenderField}
/>
)}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{/* Buttons for cancel and submit */}
<footer>
<button
type="submit"
onClick={() => formik.handleSubmit()}
disabled={!(formik.dirty && formik.isValid)}
className={cn('submit', {
active: formik.dirty && formik.isValid,
inactive: !(formik.dirty && formik.isValid),
})}
>
{t('UPDATE')}
</button>
<button onClick={() => close()} className="cancel">
{t('CLOSE')}
</button>
</footer>
<div className="btm-spacer" />
</>
)}
</Formik>
</MuiPickersUtilsProvider>
</section>
</>
);
};
const getInitialValues = (metadataFields) => {
// Transform metadata fields provided by backend
let initialValues = {};
metadataFields.mergedMetadata.forEach((field) => {
initialValues[field.id] = field.value;
});
return initialValues;
};
Here is RenderMultiField:
const childRef = React.createRef();
const RenderMultiField = ({ fieldInfo, field, form }) => {
// Indicator if currently edit mode is activated
const [editMode, setEditMode] = useState(false);
// Temporary storage for value user currently types in
const [inputValue, setInputValue] = useState('');
useEffect(() => {
// Handle click outside the field and leave edit mode
const handleClickOutside = (e) => {
if (childRef.current && !childRef.current.contains(e.target)) {
setEditMode(false);
}
};
// Focus current field
if (childRef && childRef.current && editMode === true) {
childRef.current.focus();
}
// Adding event listener for detecting click outside
window.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Handle change of value user currently types in
const handleChange = (e) => {
const itemValue = e.target.value;
setInputValue(itemValue);
};
const handleKeyDown = (event) => {
// Check if pressed key is Enter
if (event.keyCode === 13 && inputValue !== '') {
event.preventDefault();
// add input to formik field value if not already added
if (!field.value.find((e) => e === inputValue)) {
field.value[field.value.length] = inputValue;
form.setFieldValue(field.name, field.value);
}
// reset inputValue
setInputValue('');
}
};
// Remove item/value from inserted field values
const removeItem = (key) => {
field.value.splice(key, 1);
form.setFieldValue(field.name, field.value);
};
return (
// Render editable field for multiple values depending on type of metadata field
editMode ? (
<>
{fieldInfo.type === 'mixed_text' && !!fieldInfo.collection && (
<EditMultiSelect
collection={fieldInfo.collection}
field={field}
setEditMode={setEditMode}
inputValue={inputValue}
removeItem={removeItem}
handleChange={handleChange}
handleKeyDown={handleKeyDown}
/>
)}
</>
) : (
<ShowValue setEditMode={setEditMode} field={field} form={form} />
)
);
};
// Renders multi select
const EditMultiSelect = ({ collection, setEditMode, handleKeyDown, handleChange, inputValue, removeItem, field }) => {
const { t } = useTranslation();
return (
<>
<div ref={childRef}>
<div onBlur={() => setEditMode(false)}>
<input
type="text"
name={field.name}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => handleChange(e)}
placeholder={t('EDITABLE.MULTI.PLACEHOLDER')}
list="data-list"
/>
{/* Display possible options for values as dropdown */}
<datalist id="data-list">
{collection.map((item, key) => (
<option key={key}>{item.value}</option>
))}
</datalist>
</div>
{/* Render blue label for all values already in field array */}
{field.value instanceof Array &&
field.value.length !== 0 &&
field.value.map((item, key) => (
<span className="ng-multi-value" key={key}>
{item}
<a onClick={() => removeItem(key)}>
<i className="fa fa-times" />
</a>
</span>
))}
</div>
</>
);
};
// Shows the values of the array in non-edit mode
const ShowValue = ({ setEditMode, form: { initialValues }, field }) => {
return (
<div onClick={() => setEditMode(true)}>
{field.value instanceof Array && field.value.length !== 0 ? (
<ul>
{field.value.map((item, key) => (
<li key={key}>
<span>{item}</span>
</li>
))}
</ul>
) : (
<span className="editable preserve-newlines">{''}</span>
)}
<i className="edit fa fa-pencil-square" />
<i className={cn('saved fa fa-check', { active: initialValues[field.name] !== field.value })} />
</div>
);
};
export default RenderMultiField;
This is initialValues before a change and after:
InitialValues before change
InitialValues after change
This is the state of MetadataFields and FetchedValues before and after change:
State before change
State after change

Adding cardholder name in Stripe

So the problem I am facing is this. Here I have created PaymentForm with Stripe. So when I am not entering the input value of CardHolder name, and when I press the Purchase button it should display the <h1>Please enter your cardholder name</h1> but it is not doing it. I just want to create a test of Cardholder name, I know the documentation of Stripe is showing real approaches. Where could the error be located?
Payment form
import React,{useContext, useEffect, useState} from 'react'
import {CardElement, useStripe, useElements } from"#stripe/react-stripe-js"
import { CartContext } from '../../context/cart'
import { useHistory, Link } from "react-router-dom";
const PaymentForm = () => {
const { total} = useContext(CartContext)
const {cart, cartItems}= useContext(CartContext)
const history = useHistory()
const {clearCart} = useContext(CartContext)
const [nameError, setNameError]=useState(null)
const [name, setName] = React.useState("");
const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState('');
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState('');
const stripe = useStripe();
const elements = useElements();
useEffect(() => {
// Create PaymentIntent as soon as the page loads
window
.fetch("http://localhost:5000/create-payment-intent", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({items: [{cart}]})
})
.then(res => {
return res.json();
})
.then(data => {
setClientSecret(data.clientSecret);
});
}, [cart]);
const cardStyle = {
style: {
base: {
color: "#32325d",
fontFamily: 'Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#32325d"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a"
}
}
};
const handleChange = async (event) => {
setDisabled(event.empty);
setError(event.error ? event.error.message : "");
};
const handleChangeInput = async (event) => {
setDisabled(event.empty);
setNameError(event.nameError ? event.nameError.message : "");
setName(event.target.value)
};
const handleSubmit = async ev => {
ev.preventDefault();
setProcessing(true);
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement)
}
});
if (payload.error) {
setError(`Payment failed ${payload.error.message}`);
setProcessing(false);
} else {
setNameError(null)
setError(null);
setProcessing(false);
setSucceeded(true)
clearCart()
}
};
useEffect(()=>{
setTimeout(() => {
if(succeeded){
history.push('/')
}
}, 3000);
},[history, succeeded])
console.log(name)
return (
<form id="payment-form" onSubmit={handleSubmit}>
<h2>Checkout</h2>
<div className='payment__cont'>
<label>Cardholder Name </label>
<input
placeholder='Please enter your Cardholder name'
type="text"
id="name"
value={name}
onChange={handleChangeInput}
/>
</div>
<div className="stripe-input">
<label htmlFor="card-element">
Credit or Debit Card
</label>
<p className="stripe-info">
Test using this credit card : <span>4242 4242 4242 4242</span>
<br />
enter any 5 digits for the zip code
<br />
enter any 3 digits for the CVC
</p>
</div>
<CardElement id="card-element" options={cardStyle} onChange={handleChange} />
<div className='review__order'>
<h2>Review Order</h2>
<h4>Total amount to pay ${total}</h4>
<h4>Total amount of items {cartItems}</h4>
<button
className='purchase__button'
disabled={processing || disabled || succeeded}
id="submit"
>
<span id="button-text">
{processing ? (
<div className="spinner" id="spinner"></div>
) : (
"Complete purchase"
)}
</span>
</button>
<button className='edit__button'onClick={()=> {history.push('/cart')}}>Edit Items</button>
</div>
{error && (
<div className="card-error" role="alert">
{error}
</div>
)}
{nameError && (
<div className="card-error" role="alert">
<h1>Please enter yor card holder name</h1>
</div>
)}
<p className={succeeded ? "result-message" : "result-message hidden"}>
Payment succeeded
{''}
<h1>Redirecting you yo the home</h1>
</p>
</form>
);
}
export default PaymentForm
You still need to validate your name input yourself. Stripe doesn't do that for you
Your handleChangeInput handler only fires when you write to your name input, and you're treating the event as if it's fired from a Stripe component, but it's not, so try this:
// Validates name input only
const handleChangeInput = async (event) => {
const { value } = event.target;
// Disable when value is empty
setDisabled(!value);
// Set the error if value is empty
setNameError(value ? "" : "Please enter a name");
// Update form value
setName(value)
};
I'm not sure what you want to do is possible with Stripe elements as cardholder name isn't actually part of it. You'll need to handle it yourself.
An alternative way of ensuring the name field is entered prior to the card element being pressed would be to force input into cardholder name field before displaying/enabling the Stripe Card Element. This way you can be more certain the the name has been entered (and then you can do what you like with it) before the card element is pressed. You could do this a lot of ways, but for example in your render:
{name.length >= 10 ? ( //Check length of name
<CardElement id="card-element" options={cardStyle} onChange={handleChange} />
) : (
<span>Please enter your cardholder name.</span> // do nothing if check not passed
)}
This is a really simple example but it checks the length of name and then if greater than or equals ten characters makes the card element visible. You could instead use your handleChangeInput to set a boolean state (true or false) on button press or something; it would be better to make this more robust.
edit: some clarifications.

Categories