How can I extract billing details from card element during button onClick? - reactjs

We have the following function that runs when our stripe Start Subscription button is clicked:
const handleSubmit = async (ev) => {
setIsProcessing(true);
const cardElement = elements.getElement('card');
try {
// Grab Stripe Elements + Variables needed to post payment subscription
cardElement.update({ disabled: true });
const apiBaseUrl = config.url.API_URL;
const id = userData.user ? userData.user._id : null;
// Grab Billing (zip only) & Create Payment Method
const billingDetails = {
address: {
postal_code: ev.target.zip.value
}
};
const paymentMethodReq = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: billingDetails
});
// Handle Creating Payment Subscription
if (paymentMethodReq.error) { ... return } // handle bad payment
const paymentObject = { id, paymentMethod: paymentMethodReq.paymentMethod.id }; // address
const response = await Axios.post(`${apiBaseUrl}/stripe/payment_subscriptions`, paymentObject);
setIsProcessing(false);
} catch (error) {
... handle error
}
};
and our subscribe button that calls the handleSubmit function
<button
className='stripe-button'
onClick={handleSubmit}
disabled={isProcessing || !stripe}
>
Start Subscription
</button>
From our understanding of the stripe docs - https://stripe.com/docs/api/payment_methods/object?lang=node - we need to pass billing_details into the createPaymentMethod function. However, handleSubmit throws an error because ev.target.zip is undefined. (I believe) since we are submitting via a button onClick (rather than a form onSubmit), that the ev.target does not have what we want. Here is the console.log for ev, ev.target and for our cardElement:
How can the billing details be extracted from the cardElement, from within the handleSubmit function, so that they can be passed to the createPaymentMethod function?

Related

Reactjs updated prop is not shown

I tried to create a interactable map following this example here: https://docs.mapbox.com/mapbox-gl-js/example/cluster/
In my componentDidMount (where I create a mapboxgl) I implemented clickable markers, when clicked on the markers a popup appears which displays various informations.
After the click I want to call a second function (fetch) to get more data on that specific marker: this.props.getData(id);
I then want to display these data in the same popup as the other information.
My problem is that this.props.testdata is empty on the first click. If I double-click on the marker, the data appear. So my guess is that my component does not notice the change of the state/prop and therefore does not update?
How do I do that or what am I missing?
Map.js
this.map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const id = e.features[0].properties.id;
const infos = e.features[0].properties.infos;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
if (id == null) {
console.log("Missing id, cant get informations")
return;
}
this.props.getData(id);
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
App.js (getData function):
getData = (id) => {
if (id== null) {
console.log("Missing id")
return;
}
const {mapCenter, startDate, endDate} = this.state;
const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://xxx', neo4j.auth.basic("xx", "xx-xx"))
const session = driver.session()
session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
};
I am not familiar with neo4j, but it is apparent that getData(id) fetches data from a server. This is going to be an asynchronous operation, so you should add a state property to maybe show a spinner while data is being fetched?
Regarding testdata not being available, I do not see the code where it is being set.
Maybe your setState code should be:
this.setState({
testdata: data
});
//If your data prop is testdata.
As per the current setState, data property of your component state would be set with server response.
Updates:
Temporary fix for async server call:
You can change following methods and try if it fixes your issue:
this.map.on('click', 'unclustered-point', async (e) => {
// ...previous code
await this.props.getData(id);
// This forces the following code to execute synchronously. Basically it should wait for your API call to be complete
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
getData = (id) => {
//... previous code
// we return a promise to use await in the onClick handler
return session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
}
If you are still facing an issue, please create a sample app and share.
I have not yet managed to fix the original problem.
However, I have found another solution:
In my Map.js I'm calling the this.props.testdata in th UI like this:
<div className="sidebar">
info: {JSON.stringify(this.props.testdata)}
</div>

React - PayPal Button fires without checking conditions

I'm using react-paypal-express-checkout
I've to options: Cash and PayPal.
Cash working fine and checks all conditions.
But bcs PayPal is a seperate component in my CartScreen component it opens and don't check a single if conditions and opens the PayPal window
The CashButton comes with function "cashTranSuccess" it's the same function as "TranSuccess"
just without the paymentID bcs it's only needed for react-paypal-express-checkout
So what I'm looking for is, to check all TranSuccess() conditions before open the PayPal window.
PayPalButton.js
import React from 'react';
import PaypalExpressBtn from 'react-paypal-express-checkout';
export default class PayPalButton extends React.Component {
render() {
const onSuccess = (payment) => {
// Congratulation, it came here means everything's fine!
console.log('The payment was succeeded!', payment);
// You can bind the "payment" object's value to your state or props or whatever here, please see below for sample returned data
this.props.tranSuccess(payment);
};
const onCancel = (data) => {
// User pressed "cancel" or close Paypal's popup!
console.log('The payment was cancelled!', data);
// You can bind the "data" object's value to your state or props or whatever here, please see below for sample returned data
};
const onError = (err) => {
// The main Paypal's script cannot be loaded or somethings block the loading of that script!
console.log('Error!', err);
// Because the Paypal's main script is loaded asynchronously from "https://www.paypalobjects.com/api/checkout.js"
// => sometimes it may take about 0.5 second for everything to get set, or for the button to appear
};
let env = 'sandbox'; // you can set here to 'production' for production
let currency = 'EUR'; // or you can set this value from your props or state
let carttotal = this.props.carttotal; // same a s above, this is the total amount (based on currency) to be paid by using Paypal express checkout
// Document on Paypal's currency code: https://developer.paypal.com/docs/classic/api/currency_codes/
const client = {
sandbox:
'',
production: 'YOUR-PRODUCTION-APP-ID',
};
// In order to get production's app-ID, you will have to send your app to Paypal for approval first
// For sandbox app-ID (after logging into your developer account, please locate the "REST API apps" section, click "Create App"):
// => https://developer.paypal.com/docs/classic/lifecycle/sb_credentials/
// For production app-ID:
// => https://developer.paypal.com/docs/classic/lifecycle/goingLive/
// NB. You can also have many Paypal express checkout buttons on page, just pass in the correct amount and they will work!
// Style Options: https://developer.paypal.com/docs/checkout/standard/customize/buttons-style-guide/ ; https://wise.com/gb/blog/custom-paypal-button
let style = {
size: 'medium',
color: 'gold',
label: 'pay',
tagline: false,
};
return (
<PaypalExpressBtn
env={env}
client={client}
currency={currency}
total={carttotal}
onError={onError}
shipping={1}
onSuccess={onSuccess}
onCancel={onCancel}
style={style}
/>
);
}
}
CartScreen
const tranSuccess = async (payment) => {
const { paymentID } = payment;
// Check time, min amoint, for delivery add delivery fees
if (timeValidation === true) {
if (sliderDeliveryValue === 'delivery') {
if (carttotal > settings[0]?.minDeliveryAmount) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(
`Min amount${settings[0]?.minDeliveryAmount}€`,
{
position: toast.POSITION.TOP_RIGHT,
}
);
}
} else if (sliderDeliveryValue === 'pickup') {
if (carttotal > 2) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(`Min amount 2.00€`, {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('Choose delivery method', {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('closed', {
position: toast.POSITION.TOP_RIGHT,
});
}
};
<PayPalButton
carttotal={carttotal}
tranSuccess={tranSuccess}
/>
<div onClick={cashTranSuccess}>
<CashButton />
</div>
Consider using the official #paypal/react-paypal-js
An example of validation using onInit and onClick functions and the actions.enable/disable callbacks or returning a promise (actions.resolve/reject) can be found in the developer documentation. Adapt this to check whatever condition you need.

store data in firestore when Browser Tab is closed or the route is changed (react JS)

const handleDraftContracts = async () => {
console.log('/bruhhhhhhandleDraftContract');
const paragraphRef: string | any = document.getElementById('contract');
const contractDetails = {
contractName: 'House Rental',
states: {
amount: amount,
},
content: paragraphRef?.textContent,
};
await makeDraftContract(contractDetails);
};
useEffect(() => {
console.log('///////I am hreeeee');
window.addEventListener('onbeforeunload', (env) => {
handleDraftContracts();
});
return () => {
console.log('///////removing');
window.removeEventListener('onbeforeunload', handleDraftContracts);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
firestore.js
// make Draft Contracts
export async function makeDraftContract(contractDetails: object | any) {
try {
console.log("making a draft contract", contractDetails);
const draftContractRef: any = collection(db,"makeDraftContracts");
let contract = await addDoc(draftContractRef, contractDetails);
console.log("./////////makeDraftContract", contract);
} catch (error) {
console.log('////errror in contract Hanlder', error);
}
}
I want to call my handleDraftContracts method whenever user closes the tab or changes the route. I am using onbeforeunload event. The handleDraftContracts is getting called but the tab unloads before Firestore could update the collection. How can I get around this that as the user closes the tab or move to a new route, my firestore method get executed first then the tab gets unloaded ?
Try with Beacon api
https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
as 'onbeforeunload' cannot make sure you request to server has been made and requests can slow down the browser
componentWillUnmount is like that one, cannot to make long running script.

Stripe refresh data in useElement hook

In form payment user has option to change duration of premium account (e.g from 1 month to 3 months). When user change it, front sent to api query to again create paymentIntents:
return await this.stripe.paymentIntents.create({
amount: 2000 * months,
currency: 'pln',
payment_method_types: ['p24', 'card'],
receipt_email: user.email,
description: 'premium',
});
this.stripe.paymentIntents return data:
amount: 4000
client_secret: "pi_3KTDMNDbx0KEJcOR289fojfP_secret_Mh5jPfAKjvxpeZCuXmRcjilFL"
id: "pi_3KTDMNDbx0KEJcOR289fojfP"
...
The problem is with hook useElements from #stripe/react-stripe-js library. Inside this hook still I have old id and when I click "Pay" I have old amount it's 2000, should be 6000.
How I can refresh data in useElements when user change count of months?
PS: I'm using this docs. Select 'Custom payment flow' and in backend I have Node (Nest.js), in front I have React.
Update:
Stripe method for payment:
import {
PaymentElement,
useStripe,
useElements,
} from '#stripe/react-stripe-js'
const Stripe = () => {
const stripe = useStripe()
const elements = useElements()
const sendPaymentDetails = async data => {
if (!stripe || !elements) return
setIsLoading(true)
const { name, email } = data
await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${APPLICATION_URL}premium`,
payment_method_data: {
billing_details: {
name,
email,
},
},
},
})
}
return (
<form onSubmit={sendPaymentDetails}>
<PaymentElement />
</form>
)
}
and this is method to create paymentIntent:
const [stripeOptions, setStripeOptions] = useState(null)
const paymentIntent = async months => {
const { data } = await createPaymentIntent(months);
setStripeOptions(data);
};
when I create new paymentIntent, should update too useElement data
I found resolve.I should update paymentIntents, not create new paymentIntents. So I add method:
return await this.stripe.paymentIntents.update(id, {
amount: this.amount * months,
});
from this docs
Now inside useElements hook I have correctly data

Update a React class component when Next Js Link used

Original Post
I have a React class component that is used on a Next/JS Dynamic Path. On initial load all is fine. But there is a fringe use case that if I am on a page where that component is already mounted and then click a Link that is the same dynamic base as the component path but with different params, the component does not unmount and getIntialProps runs but it does not call the constructor to update the new state.
Path Examples:
Dynamic Pattern
/vos/[id]/[slug]
Initial Path
/vos/203dk2-d33d-3e3e3d/thisName
New path
/vos/554-34r4f-44d4e/aNewName
Events
1. component loads with initial props/state
2. user clicks Link to same component path but updated params
3. component lifecycles run to reset state but does not and does not unmount
4. 2-5 seconds pass... no activity
5. getIntialProps finally runs with new params
6. componentDidUpdate lifecycle called to update state with new props
I have also tried to change the Link to a Router.push() but have the same results.
Question:
Is there a way to force unmount a component to allow for a new instance of it to be created.
If not above, what would be the best way to handle this fringe case in the component lifecyces? I have tried to update the state with a function in the componentDidUpdate() cycle but this gets a bit messy as it runs before the SSR is called so state management gets out of sync.
Code Example
static getInitialProps = async (ctx: NextPageContext) => {
const services = VerifiedOrganizationProfilePage.PageServices(ctx);
const { lang: language, id: voId } = ctx.query as IProfilePagesQueryParams;
// check VO page is existing
// if VO owner or Admin
let verifiedOrganization: IVerifiedOrganizationResponse | undefined;
let user: IUserResponse | undefined;
let isVoOwner: boolean = false;
let isPlatformAdmin: boolean = false;
let isVoOwnerOrPlatformAdmin: boolean = false;
try {
verifiedOrganization = await services.verifiedOrganizationService.getVerifiedOrganization(voId);
if (!verifiedOrganization) throw new Error('No verified organization with that id was found!');
const userId = await services.cognitoIdentityService.getUserIdInSession();
if (userId) {
user = await services.usersService.getUser(userId);
isPlatformAdmin = AuthUtil.hasRoles(
[ERole.PLATFORM_ADMIN],
user.platformRoles
);
isVoOwner = OrganizationUtil.isVerifiedOrganizationOwner(
verifiedOrganization.id,
user
);
isVoOwnerOrPlatformAdmin = isVoOwner || isPlatformAdmin;
}
} catch (error) {
NextUtil.redirectTo(
'/not-found',
ctx.res,
HTTP_REDIRECT.TEMPORARY,
language
);
}
// fetch publicly visible data
const { store } = ctx;
store.dispatch(fetchCampaignsRequest({
verified_organization_id: voId,
limit: isVoOwnerOrPlatformAdmin ? EPaginationLimit.FIVE_HUNDRED : EPaginationLimit.DEFAULT,
}, ctx));
store.dispatch(fetchCausesRequest({
verified_organization_id: voId,
limit: EPaginationLimit.DEFAULT
}, ctx));
store.dispatch(fetchCommentsRequest({
verified_organization_id: voId,
limit: EPaginationLimit.DEFAULT
}, ctx));
store.dispatch(fetchUpdatesRequest({
verified_organization_id: voId,
limit: EPaginationLimit.DEFAULT
}, ctx));
// wait for redux saga updating state
await new Promise<void>((resolve) => {
const unsubscribe = store.subscribe(() => {
const state = store.getState();
if (!state.campaign.isFetching && !state.cause.isFetching && !state.comment.isFetching && !state.update.isFetching) {
unsubscribe();
resolve();
}
});
});
return {
user,
voId,
isVoOwner,
isPlatformAdmin,
verifiedOrganization,
isVoOwnerOrPlatformAdmin,
tabValue: EVerifiedOrganizationProfilePageTabs.CAMPAIGNS,
pageUrl: NextUtil.getPageUrl(ctx),
};
}
...
constructor(props){
super(props);
this.state = {...this.props}
}
...
// used to check new props from VO if coming from another VO page and set the state
static async getDerivedStateFromProps(nextProps: IVerifiedOrganizationProfilePageProps, prevState: IVerifiedOrganizationProfilePageState) {
if (nextProps.voId !== prevState.voId) {
return {
voId: nextProps.voId,
urlChanged: true,
tabValue: EVerifiedOrganizationProfilePageTabs.CAMPAIGNS,
isWaitingAdminApproval: false,
isUserBothVoAndIpRepresentative: false,
visibleBeneficiaryList: listResponse,
beneficiaryGroups: listResponse,
followingVerifiedOrganizations: {},
beneficiaryBlockchainCSVData: undefined,
userRating: undefined,
isLoading: true,
};
}
}
...
async componentDidMount() {
Router.events.on('routeChangeStart', this.handleRouteChangeComplete); // to trigger callback beofre NEXT Router/Link executes
await this.fetchPersonalData(); // method to fetch user specific data
}
...
async componentDidUpdate() {
if (this.state.urlChanged) {
await this.fetchPersonalData();
}
}
...
componentWillUnmount() {
Router.events.off('routeChangeStart', this.handleRouteChangeComplete);
}
...
// sets the current open tab to CAMPAIGNS if a VO navigates to a connected VO profile from a restricted tab
public handleRouteChangeComplete = async (url: string) => {
this.setState({tabValue: EVerifiedOrganizationProfilePageTabs.CAMPAIGNS,});
}
...
public fetchPersonalData = async () => {
const { voId, user, verifiedOrganization, isPlatformAdmin, isVoOwnerOrPlatformAdmin } = this.props;
let isVoRepresentative: boolean = false;
let isIpRepresentative: boolean = false;
let isUserBothVoAndIpRepresentative: boolean = false;
let isWaitingAdminApproval: boolean = false;
let visibleBeneficiaryList: IListResponse<IBeneficiaryWithInvitationStatus> | undefined;
let beneficiaryGroups: IListResponse<IGroup> | undefined;
try {
const services = VerifiedOrganizationProfilePage.PageServices();
if (user) {
isWaitingAdminApproval = verifiedOrganization.verifiedOrganizationStatus === EVerifiedOrganizationStatus.PENDING_PLATFORM_ADMIN_APPROVAL;
// If Verified Organization is waiting for Admin Platform approval, only Platform Admin can see the page.
if (isWaitingAdminApproval && !isPlatformAdmin) {
throw new NotFoundError();
}
isVoRepresentative = AuthUtil.hasRoles(
[ERole.VERIFIED_ORGANIZATION_REPRESENTATIVE],
user.platformRoles
);
isIpRepresentative = AuthUtil.hasRoles(
[ERole.IMPLEMENTING_PARTNER_REPRESENTATIVE],
user.platformRoles
);
isUserBothVoAndIpRepresentative =
isVoRepresentative && isIpRepresentative;
// If Verified Organization is waiting for Admin Platform approval, only Platform Admin can see the page.
if (isWaitingAdminApproval && !isPlatformAdmin) {
throw new NotFoundError();
}
// add the prefix to the id so we can match the record in the Connections table.
const prefixedId = EIdTypes.VERIFIED_ORGANIZATION.toUpperCase() + '#' + verifiedOrganization.id;
// Fetch data visible only to VoOwner and Aidonic
const connections = [] as unknown as IListResponse<IConnectionVOIP>;
if (isVoOwnerOrPlatformAdmin) {
// Get from the API all the connections sent or received
// Commenting this out as it calling twice the API. The call to the API is done from the Tab instead.
// connections = await services.connectionsService.getVisibleConnectionsByOrganization(prefixedId);
visibleBeneficiaryList = await services.beneficiaryService.getBeneficiariesVisibleToOrganization(prefixedId);
beneficiaryGroups = await services.beneficiaryGroupsService.getBeneficiaryGroupsList(prefixedId, {limit: EPaginationLimit.THIRTY});
}
const follows = await services.followsService.getFollowsList({
user_id: user.id
});
const [followingVerifiedOrganizations] = mapFollowsByKey(follows, [
'verifiedOrganizationId'
]);
const userRating = await services.ratingsService.getRatingList({
user_id: user.id,
verified_organization_id: verifiedOrganization.id
});
this.setState({
voId,
connections,
tabValue: EVerifiedOrganizationProfilePageTabs.CAMPAIGNS,
beneficiaryGroups,
isWaitingAdminApproval,
visibleBeneficiaryList,
followingVerifiedOrganizations,
isUserBothVoAndIpRepresentative,
userRating: userRating && userRating[0],
isLoading: false,
urlChanged: false
});
}
} catch (e) {
console.log('Error in data fetching on VO profile page: ', e);
}
}
Update
I have split the props from the state to maintain one source of true and used getDerivedStateFromProps() to catch the change and call fetchPersonalData(). All working well.
The only issue is it seems to take twice as long to load the new updated props/state than initial load. Thoughts?
Solution:
For my case this was cause by a API call in the lifecycle. Framework worked fine.

Resources