I am working with web3-react and I cannot figure out how to keep the connection to the MetaMask wallet persistent upon browser refreshes.
This is the code:
// define the injectedConnectors
const injectedConnector = new InjectedConnector({
supportedChainIds: [
1, // Mainet
3, // Ropsten
4, // Rinkeby
5, // Goerli
42, // Kovan
],
})
const { chainId, account, activate, active } = useWeb3React()
// activate the wallet
activate(injectedConnector)
console.log(account)
// all good.
Up to here all is working and I activate my MetaMask wallet as well as I get the account correctly logged, and the active variable is a boolean that changes to true.
The problem is that when I refresh the page the active turns to false and I lose the connection between the UI to the MetaMask wallet. Of course saving active into the browser does not change anything because the connection relies on the active boolean value.
The docs are lacking such information.
Finally found a solution!
I was trying to use the example in the official library using ... but for some reason it wasn't working though no error came out.
Then I stumbled upon some guy who had the same issue and posted on reddit and got a good answer that works for me.
This is the link to the post: https://www.reddit.com/r/ethdev/comments/nw7iyv/displaying_connected_wallet_after_browser_refresh/h5uxl88/?context=3
and this is the code from that post:
First create a file that holds the injectedConnector called connectors.js:
import { InjectedConnector } from '#web3-react/injected-connector'
export const Injected = new InjectedConnector({ supportedNetworks: [1, 3, 4, 5, 42] })
Then create a component that checks if the user already activated the wallet:
import React, { useEffect, useState } from 'react'
import { injected } from '../connectors'
import { useWeb3React } from '#web3-react/core'
function MetamaskProvider({ children }) {
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React()
const [loaded, setLoaded] = useState(false)
useEffect(() => {
injected
.isAuthorized()
.then((isAuthorized) => {
setLoaded(true)
if (isAuthorized && !networkActive && !networkError) {
activateNetwork(injected)
}
})
.catch(() => {
setLoaded(true)
})
}, [activateNetwork, networkActive, networkError])
if (loaded) {
return children
}
return <>Loading</>
}
export default MetamaskProvider
And wrap MetamaskProvider around the components you want the wallet to be activated upon refresh:
return (
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
<StylesProvider injectFirst>
<Paper>
<Router>
<Web3ReactProvider getLibrary={getLibrary}>
<MetamaskProvider>
{...children components}
</MetamaskProvider>
</Web3ReactProvider>
</Router>
</Paper>
</StylesProvider>
</ThemeProvider>
);
Its actually really simple. You can just store the connect address in local storage and when the user clicks the disconnect button then remove the address from local storage. basically we use the condition that if there is an acccount in local storage then we connect on load and if not then we have to manually click the connect button. Consider the code below. Note that ideally you should write the logic as a hook and use the hook in the main app then pass in the props the "active" status which is returned from useWeb3React(). but for the purpose of this example i just keep the connect logic in one file to make it read easier
import React, { useState, useEffect } from 'react';
import Web3 from 'web3';
import detectEthereumProvider from '#metamask/detect-provider';
import { useWeb3React } from "#web3-react/core"
import { InjectedConnector } from '#web3-react/injected-connector'
//declare supportated chains
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42, 1337, 43114],
})
export default function connButton() {
var web3;
var accounts;
var connected
const [loading, setLoading] = useState(false)
//here we can destructure out various things from web3React such as
//active (which is true if the user is connected and false otherwise)
//activate and deactiveate which we use to instansiate and break the users
//connection
const { active, account, library, connector, activate, deactivate } = useWeb3React()
//set up an elemnt in local storage that we use to hold the connected account
var acc = localStorage.getItem("account")
//function that initialises web3.js
const connectWalletHandler = () => {
if (window.ethereum && window.ethereum.isMetaMask) {
console.log('MetaMask Here!');
web3 = new Web3(window.ethereum);
window.ethereum.request({ method: 'eth_requestAccounts'})
} else {
console.log('Need to install MetaMask');
// setErrorMessage('Please install MetaMask browser extension to interact');
}
console.log(web3.eth.currentProvider)
}
//function that is called on page load if and only if their exists and
//item for the user accoun tin local storage
async function connectOnLoad() {
try {
//here we use activate to create the connection
await activate(injected)
connected = true
} catch (ex) {
console.log(ex)
}
//we use web3.eth to get the accounts to store it in local storage
var accounts1 = await web3.eth.getAccounts();
acc = localStorage.setItem("account", accounts1);
}
//here we use a useEffect so that on page load we can check if there is
//an account in local storage. if there is we call the connect onLoad func
//above which allows us to presist the connection and i also call connectWalletHandler
which sets up web3.js so we can call web3.eth.getAccounts()
useEffect(() => {
if (acc != null) {
connectOnLoad()
}
connectWalletHandler()
}, [])
//however in the case where there is no item in local storage we use this
//function to connect which is called when we click the connect button. its
//essentially the same but we check if local storage is null if it is we activate
//if its not then we disconnect. And when we disconnect we remove the acccount from local storage
async function connectOnClick() {
if (localStorage.getItem("account") == null) {
setLoading(true);
try {
await activate(injected)
connected = true
} catch (ex) {
console.log(ex)
}
// window.location.reload();
var accounts1 = await web3.eth.getAccounts();
console.log(accounts1)
acc = localStorage.setItem("account", accounts1);
console.log(acc)
setTimeout(function(){
setLoading(false)
}, 1600);//wait 2 seconds
} else {
disconnect();
connected = false
}
}
async function disconnect() {
try {
deactivate()
localStorage.removeItem("account");
} catch (ex) {
console.log(ex)
}
}
return (
//remember the active boolean from useReactWeb3() stores a bool
//depending on if the user is or is not connected there for we can
//use this as a conditon to render the button saying "Connect Wallet"
or displaying their address as the text.
<div>
{active ? <button onClick={connectOnClick}>{account.substring(0, 6)}...{account.substring(account.length - 4)}</button> : <button onClick={connectOnClick}>Connect Wallet</button>}
</div>
);
}
then in your app.js remember to wrap your entire app in the tag. remember this means you need to import web3React into your app.js also
Related
I have a React app which I am testing on Vercel.
The app was created using create-react-app.
I have a Stripe Account and a Django REST API, hosted on Heroku.
I’m using PaymentRequestButtonElement to generate a dynamic Apple / Google Pay button.
I’m testing Google Pay via chrome with my personal card details saved into the browser.
The price is fetched from my django app, each product is a ‘Card’, within which the payment button appears, with the price of that product passed in.
The payments go through just fine and the payment shows up in my Stripe dashboard as successful.
My /create-payment-intent/ seems functional as the payment goes through and that it generates a client secret when I test it in Postman.
The issue is simply that the React app doesn’t seem to be listening for the payment success or failure, as it doesn’t say so on the browser console or on the UI.
The google pay sheet times out and the following is printed to the console:
DEVELOPER_ERROR in loadPaymentData: An error occurred in call back, please try to avoid this by setting structured error in callback response H # pay.js:149
Here is my React component.
CheckoutForm.js
import React, { useEffect, useState } from 'react';
import {
PaymentRequestButtonElement,
useStripe,
} from '#stripe/react-stripe-js';
import axios from 'axios';
const CheckoutForm = (props) => {
const stripe = useStripe();
const [paymentRequest, setPaymentRequest] = useState(null);
const [paymentSuccess, setPaymentSuccess] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
// const price = props.price;
// const dollar_price = price * 100;
useEffect(() => {
if (stripe) {
const pr = stripe.paymentRequest({
country: 'US',
currency: 'usd',
total: {
label: 'Purchase',
amount: 100,
},
requestPayerName: true,
requestPayerEmail: true,
applePay: true,
googlePay: true,
});
pr.canMakePayment().then(result => {
if (result) {
setPaymentRequest(pr);
}
});
}
}, [stripe]);
useEffect(() => {
if (paymentRequest) {
paymentRequest.on('paymentmethod', async event => {
const paymentMethod = event.paymentMethod;
try {
const response = await axios.post(
'https://my-api.com/create-payment-intent/',
{
paymentMethodId: paymentMethod.id,
amount: 100,
automatic_payment_methods: {
'enabled': true,
},
currency: 'usd',
}
);
const pi = await stripe.confirmCardPayment(response.data.client_secret, {
payment_method: paymentMethod.id
});
if (pi.status === 'succeeded') {
event.complete();
console.log('Payment succeeded!');
setPaymentSuccess(true);
setErrorMessage(null);
setSuccessMessage("Payment succeeded!");
} else if (pi.status === 'requires_action' || pi.status === 'requires_confirmation') {
event.complete('success');
console.log('Additional steps required!');
setErrorMessage(null);
setSuccessMessage("Additional steps required, please check your email for further instructions.");
// Prompt user to complete additional steps
} else if (pi.status === 'requires_payment_method') {
event.complete('fail');
console.log('Payment method required!');
setErrorMessage("Payment method required. Please add a new payment method.");
// Prompt user to add a new payment method
} else if (pi.status === 'processing') {
event.complete('success');
console.log('Payment is being processed!');
setErrorMessage(null);
setSuccessMessage("Payment is being processed. Please wait.");
// Show a message to the user that the payment is being processed
} else if (pi.status === 'canceled') {
event.complete('fail');
console.log('Payment canceled!');
setErrorMessage("Payment canceled.");
// Show a message to the user that the payment was canceled
} else if (pi.status === 'failed') {
event.complete('fail');
console.log('Payment failed!');
setErrorMessage("Payment failed. Please check your information and try again.");
// Show a message to the user that the payment failed
}
} catch (error) {
event.complete('fail');
console.log('An error occurred:', error);
setErrorMessage("An error occurred. Please try again later.");
// Show an error message to the user
}
});
}
}, [paymentRequest, stripe]);
if (paymentRequest) {
return <>
{paymentSuccess && <p>Payment Successful!</p>}
<PaymentRequestButtonElement options={{ paymentRequest }} />
</>
}
return 'Insert your form or button component here.';
};
export default CheckoutForm;
Here is my index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Elements} from '#stripe/react-stripe-js';
import {loadStripe} from '#stripe/stripe-js';
import CheckoutForm from './CheckoutForm';
// Make sure to call `loadStripe` outside of a component's render to avoid
// recreating the `Stripe` object on every render.
const stripePromise = loadStripe('pk_live_123');
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
</React.StrictMode>
);
reportWebVitals();
And here is my django view
class PaymentIntentView(APIView):
def post(self, request, *args, **kwargs):
amount = request.data.get('amount')
currency = request.data.get('currency')
automatic_payment_methods = request.data.get('automatic_payment_methods')
try:
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
automatic_payment_methods={
'enabled': True,
},
# You can also add other options like capture_method, setup_future_usage, etc.
)
return Response({'client_secret': intent.client_secret, 'id': intent.id})
except Exception as e:
return Response({'error': str(e)})
I feel like I’m missing a step here.
Happy to share more info if necessary.
I'm expecting the console and the UI to produce a success or failure message, but it just times out. The payment appears as successful in my Stripe Dashboard.
Inside the function paymentRequest.on('paymentmethod'... you have the call to your endpoint "https://my-api.com/create-payment-intent/" then a subsequent JS call to const pi = await stripe.confirmCardPayment.
From what you described, it looks like some bits are off around this area. You can put breakpoints on the endpoint call and the confirmCardPayment to see if they really go through. You can also open your browser console to see whether the real ajax call succeeded (instead of testing in Postman).
If they are succeeded, what is the pi.status on your console.log?
I'm trying to integrate a Stripe payments page using the React "Elements". I'm following the tutorial from https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements#web-submit-payment and I've gotten to step 5, "Submit the payment to Stripe". My code doesn't look much different from the example, but whenever I try to submit a payment this error:
Invalid value for stripe.confirmPayment(): elements should have a mounted Payment Element.
Thrown from stripe.confirmPayment. I've included the <Elements/> and <PaymentElement/> on the page, and passed the return value from useElements() so I'm really not sure what I'm missing.
Here's my checkout form:
function StripeCheckoutForm({paymentIntent,booking}: StripeCheckoutFormProps) {
const stripe = useStripe();
const elements = useElements();
const [confirming,setConfirming] = useState(false)
const handleSubmit = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!stripe || !elements) {
notifyWarning("Still loading. Please wait a few seconds and then try again.");
return;
}
setConfirming(true)
try {
const {error} = await stripe.confirmPayment({
//`Elements` instance that was used to create the Payment Element
elements,
confirmParams: {
return_url: resolveRoute('customerPaymentReceived',{key:booking.key}),
},
})
if(error) {
notifyError(error.message)
setConfirming(false)
}
} catch(error) {
setConfirming(false)
if(error?.message) {
notifyError(error.message) // <-- error shown here
} else {
notifyError("Something went wrong");
}
}
}
return (
<form onSubmit={handleSubmit}>
<PaymentElement/>
<BlockSpacer height="1rem"/>
<ActionButton disabled={!stripe || !elements || confirming} type="submit" className="btn-phone btn-fullwidth">Pay {paymentIntent.amountFormatted}</ActionButton>
</form>
)
}
And that form is inside this component, similar to the example shown in step 4:
import {Elements as StripeElements} from '#stripe/react-stripe-js';
import {useStripe, useElements, PaymentElement} from '#stripe/react-stripe-js';
function StripePaymentForm({stripeAccountId,paymentIntent,booking}: StripePaymentFormProps) {
const options = {
clientSecret: paymentIntent.clientSecret,
loader: 'always',
}
return (
<StripeElements stripe={getStripeConnect(stripeAccountId)} options={options}>
<StripeCheckoutForm paymentIntent={paymentIntent} booking={booking}/>
</StripeElements>
)
}
The only thing I can see that's different is that I'm using a Connect account, so I'm passing in account ID in when I load Stripe. getStripeConnect is basically
loadStripe(STRIPE_PUBLIC_KEY, {stripeAccount: CONNECTED_ACCOUNT_ID})
You can see the React component tree here if it helps:
What am I missing?
I'm guessing this stems from useElements() is not finding any elements:
But I still don't know why.
I believe it's a recent bug in react-stripe-js:
https://github.com/stripe/react-stripe-js/issues/296
I believe your loadStripe() promise isn't resolved at the time it is passed to the Elements provider, thus useElements returns null. Either load it earlier or await it and it should resolve your issue.
as the title says I can't make hellosign-embedded work with next
My app will be served though a CDN, so I don't cake if it won't work with SSR
const HelloSign: any = dynamic(
(): any => {
return import("hellosign-embedded")
},
{ ssr: false }
)
export default function Home() {
const client =
typeof window !== "undefined"
? new HelloSign({
allowCancel: false,
clientId: "HELLO SIGN CLIENT ID", // DEV HelloSign Client ID
skipDomainVerification: true,
})
: null
return null
}
I keep getting TypeError: HelloSign is not a constructor
I also created this issue on GitHub with further information
Edit:
I see now where things got confusing. hellosign-embedded references the window object (i.e., the import needs to be visible only during the client-side compilation phase). I saw that next/dynamic was being used to import a module. Since dynamic() returns a type of ComponentType and the containing client variable was inside the Home function, I had incorrectly assumed you were trying to import a React component.
It turns out hellosign-embedded isn't even a React component at all so you shouldn't use next/dynamic. You should be able to use ES2020 imports instead. Your pages/index.tsx file will look something like this:
import type { NextPage } from "next";
const openDocument = async () => {
// ES2020 dynamic import
const HelloSign = (await import("hellosign-embedded")).default;
const client = new HelloSign({
allowCancel: false,
clientId: "HELLO SIGN CLIENT ID", // DEV HelloSign Client ID
skipDomainVerification: true,
});
client.open("https://www.example.com");
};
const Home: NextPage = () => {
return <button onClick={() => openDocument()}>Open</button>;
};
export default Home;
disregard old answer:
HelloSign is of type ComponentType, so you can use the JSX element stored in the variable directly:
// disregard: see edit above
// export default function Home() {
//
// const client =
// typeof window !== "undefined" ? (
// <HelloSign
// allowCancel={false}
// clientId="HELLO SIGN CLIENT ID" // DEV HelloSign Client ID
// skipDomainVerification={true}
// />
// ) : null;
//
// return client;
//
// };
I can't clear context even when I'm setting the state to null in the context file. Can anyone tell me what's wrong with my code? This is my code:
import React, { createContext, useState } from 'react';
export const MembersContext = createContext([{}, () => {}]);
export const MembersProvider = ({ children }) => {
const [members, setMembers] = useState(null);
const refreshMembers = async () => {
try {
const data = await request('api/members');
setMembers(data);
} catch (error) {
console.log('ERROR: ', error);
}
};
const clearMembers = () => {
setMembers(null);
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared
};
return (
<MembersContext.Provider
value={{
members,
clearMembers,
}}
>
{children}
</MembersContext.Provider>
);
};
Then in my sign out page I have a button to use the clear context function:
import React, { useContext } from 'react';
import {
Button,
} from 'react-native';
import { MembersContext } from '../MembersContext';
const Settings = () => {
const { clearMembers, members } = useContext(MembersContext);
const clearContext = () => {
clearMembers();
console.log('CLEARED MEMBERS?: ', members); // not cleared
//logout()
};
return (
<Button onPress={()=> clearContext()}>Log Out</Button>
);
};
export default Settings;
My console log and screen still shows the data from the previous session.
Let's see both scenarios :-
console.log('CLEARED MEMBERS?: ', members); // not cleared - Here you're not logging the value of members that will get updated but the value of members on which clearContext() closed over i.e. the current state value (before update).
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared - This isn't the right way to see if members changed. The state
update is async. Doing a console.log(...) just after updating
members won't work. And I think the reason is same as above. It won't work because the clearMembers() function closes over current value of members.
Each update to members also result's in a new clearMembers()/clearContext() (due to re-render), atleast in this case. That's why these functions can always access the latest state.
To check whether members actually updated, log the value either in the function body of Settings or inside useEffect with members in it's dependency array.
Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.