So I'm using firebase as my database and hosting service and I've written the application in React.
Now I'm trying to add Stripe to the application so I can charge customers. But there is an issue, Stripe seems to need a separate server, which is a bit of an issue because I'm using Firebase.
I've read the documentation (here - https://firebase.google.com/docs/use-cases/payments) and watched the videos from firebase (here - https://www.youtube.com/watch?v=BrLTF4QdRrM and here - https://www.youtube.com/watch?v=BrLTF4QdRrM) and I'm still lost.
I have no idea where I'm meant to include the Stripe Publishable Key and I've worked myself into such a knot I'm going to have to spend the next few hours undoing the last few hours of effort.
If you can point me in the direction of a simple guide or just tell me where the Publishable Key needs to go, that would be hugely helpful!
This may confuse more than help but here is the code that I've created to create a stripe customer and stripe charge etc.
mport * as stripe from "stripe";
import * as functions from "firebase";
import getFirestoreDb from "../firebase/firestore/getFirestoreDb";
// import {firestore} from "#firebase/firestore/dist/packages/firestore/test/util/api_helpers";
// const admin = require('firebase-admin');
// admin.initializeApp();
// const logging = require('#google-cloud/logging')();
// const currency = functions.config().stripe.currency || 'USD';
const db = getFirestoreDb();
//START CUSTOMER CHARGE //
// Charge the Stripe customer whenever an amount is created in Cloud Firestore
export const createStripeCharge = functions.firestore
.document('stripe_customers/{userId}/charges/{autoId}')
.onCreate(async ( snap, context) => {
const val = snap.data();
try {
//Look up the Stripe customer id written in createStripeCustomer
const snapshot = await
db.collection('stripe_customers')
.doc(context.params.userId).get();
const snapval = snapshot.data();
const customer = snapval.customer_id;
// Create a charge using the pushId as the idempotency key
// protecting against double charges
const amount = val.amount;
const idempotencyKey = context.params.id;
const currency = "USD";
const charge = {amount, currency, customer};
if (val.source !== null) {
charge.source = val.source;
}
// setSourceOrDefault(charge, snap.data().source)
const response = await stripe.charges.create(charge, {idempotency_key: idempotencyKey} );
//if the result is successful, write it back to the database
return snap.ref.set(response, {merge:true});
} catch(error) {
// We want to capture errors and render them in a user-friendly way, while
// still logging an exception with StackDriver
await snap.ref.set({error: userFacingMessage(error)}, {merge:true});
// return reportError(error, {user: context.params.userId});
}
});
// END CUSTOMER CHARGE //
// When a user is created, register them with Stripe
export const createStripeCustomer = functions.auth.user().onCreate(async (user) => {
const customer = await stripe.customers.create({email: user.email});
return db.collection('stripe_customers').doc(user.uid).set({customer_id: customer.id});
});
// Add a payment source (card) for a user by writing a stripe payment source token to Cloud Firestore
export const addPaymentSource = functions.firestore.document('/stripe_customers/{userId}/tokens/{pushId}').onCreate(async (snap, context) => {
const source = snap.data();
const token = source.token;
if (source === null){
return null;
}
try {
const snapshot = await db.collection('stripe_customers').doc(context.params.userId).get();
const customer = snapshot.data().customer_id;
const response = await stripe.customers.createSource(customer, {source: token});
return db.collection('stripe_customers').doc(context.params.userId).collection("sources").doc(response.fingerprint).set(response, {merge: true});
} catch (error) {
await snap.ref.set({'error':userFacingMessage(error)},{merge:true});
// return reportError(error, {user: context.params.userId});
}
});
// When a user deletes their account, clean up after them
export const cleanupUser = functions.auth.user().onDelete(async (user) => {
const snapshot = await db.collection('stripe_customers').doc(user.uid).get();
const customer = snapshot.data();
await stripe.customers.del(customer.customer_id);
return db.collection('stripe_customers').doc(user.uid).delete();
});
// To keep on top of errors, we should raise a verbose error report with Stackdriver rather
// than simply relying on console.error. This will calculate users affected + send you email
// alerts, if you've opted into receiving them.
// // [START reporterror]
// function reportError(err, context = {}) {
// // This is the name of the StackDriver log stream that will receive the log
// // entry. This name can be any valid log stream name, but must contain "err"
// // in order for the error to be picked up by StackDriver Error Reporting.
// const logName = 'errors';
// const log = logging.log(logName);
//
// // https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/MonitoredResource
// const metadata = {
// resource: {
// type: 'cloud_function',
// labels: {function_name: process.env.FUNCTION_NAME},
// },
// };
//
// // https://cloud.google.com/error-reporting/reference/rest/v1beta1/ErrorEvent
// const errorEvent = {
// message: err.stack,
// serviceContext: {
// service: process.env.FUNCTION_NAME,
// resourceType: 'cloud_function',
// },
// context: context,
// };
//
// // Write the error log entry
// return new Promise((resolve, reject) => {
// log.write(log.entry(metadata, errorEvent), (error) => {
// if (error) {
// return reject(error);
// }
// return resolve();
// });
// });
// }
// [END report error]
function userFacingMessage(error) {
return error.type ? error.message : 'An error has occurred, developers have been alerted';
}
stripe.setPublishableKey("STRIPE_KEY");
You have to set the publishable key on the client side.
For example, using React Native and tipsi-stripe (stripe client sdk) you can do as following:
import stripe from 'tipsi-stripe'
stripe.setOptions({
publishableKey: 'YOUR-PUBLISHABLE-KEY',
})
You just need to initialize the stripe client sdk you are using with your publishableKey.
Related
I have a nextjs project and I want to replace the twilio programmable chat with twilio conversations.
I did the following steps:
I did one API which creates or gets (if it is already created) the conversation and returns to the client the conversation unique name and the token
Once I have the conversation unique name and the token I want to send client side one message.
To do so I did the following function:
import { Client, State } from '#twilio/conversations';
import toast from 'react-hot-toast';
const sendMessageToConversation = async (
token: string,
room: string,
message: string
) => {
const client = new Client(token);
client.on('stateChanged', async (state: State) => {
if (state === 'initialized') {
try {
const conversation = await client.getConversationByUniqueName(room);
await conversation.join();
if (message && String(message).trim()) {
await conversation.sendMessage(message);
}
} catch {
toast.error('Unable to create conversation, please reload this page');
}
}
});
};
the problem seems to be const conversation = await client.getConversationByUniqueName(room); which gives the following error:
What do you think I did wrong?
Also is it a better idea to build an API to send messages in this way? I would avoid this because of the possible overhead of the server
UPDATE
I tried to send a message through API. It works and it returns what I expect. For more detail I will put also the code I have on backend side which generates the tokens and the conversations.
I generate tokens for client side with:
import Twilio from 'twilio';
import { config } from '../config';
const client = require('twilio')(
config.TWILIO_ACCOUNT_SID,
config.TIWLIO_AUTH_TOKEN
);
const AccessToken = Twilio.jwt.AccessToken;
const ChatGrant = AccessToken.ChatGrant;
const SyncGrant = AccessToken.SyncGrant;
export const tokenGenerator = (identity: string) => {
const token = new AccessToken(
config.TWILIO_ACCOUNT_SID,
config.TWILIO_API_KEY,
config.TWILIO_API_SECRET
);
token.identity = identity || 'unknown';
if (config.TWILIO_CHAT_SERVICE_SID) {
const chatGrant = new ChatGrant({
serviceSid: config.TWILIO_CHAT_SERVICE_SID,
pushCredentialSid: config.TWILIO_FCM_CREDENTIAL_SID,
});
token.addGrant(chatGrant);
}
if (config.TWILIO_SYNC_SERVICE_SID) {
const syncGrant = new SyncGrant({
serviceSid: config.TWILIO_SYNC_SERVICE_SID || 'default',
});
token.addGrant(syncGrant);
}
return {
identity: token.identity,
token: token.toJwt(),
};
};
I create conversations with:
const client = require('twilio')(
config.TWILIO_ACCOUNT_SID,
config.TIWLIO_AUTH_TOKEN
);
export const createTwilioConversation = async (
partecipantsProfiles: Partial<User>[],
identity: string
) => {
const friendlyName: string = partecipantsProfiles
.map((el) => `${el.first_name} ${el.last_name}`)
.join(' - ');
const conversation = (await client.conversations.conversations.create({
friendlyName,
uniqueName: uuidv4(),
})) as TwilioConversationResponse;
await client.conversations
.conversations(conversation.sid)
.participants.create({ identity });
return conversation;
};
The flow I do in order to send messages is:
If I want to send a message I create a conversation by calling an API, executes the functions above and returns the room unique name and the token. I also store into my DB the room unique name, participants and other infos. I do this only for the first message. If a user has already chatted with another user, then I don't create a conversation anymore, but I return the unique name stored + token generated and I get/send the message client side
I have also tried to send a message through API and it works. I can't figure out why I still can't get the conversation by unique name client side. Maybe I should generate the token differently?
This is the method that sends messages server side:
export const sendMessage = async (
conversationSid: string,
author: string,
body: string
) => {
return await client.conversations
.conversations(conversationSid)
.messages.create({ author, body });
};
Your issue is that you are not using the default Conversations service, but you are not scoping your API calls on the server side to the service you need.
So in pages/api/utils/conversations you need to add .services(config.TWILIO_CHAT_SERVICE_SID) into all your API calls. See below:
export const createTwilioConversation = async (
chatTo: string,
myIdentity: string
) => {
const uniqueName = uuidv4();
const conversation = (await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations.create({
friendlyName: `Chat created by ${myIdentity}`,
uniqueName,
})) as TwilioConversationResponse;
await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversation.sid)
.participants.create({ identity: chatTo });
await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversation.sid)
.participants.create({ identity: myIdentity });
return conversation;
};
export const sendMessage = async (
conversationSid: string,
author: string,
body: string
) => {
return await client.conversations
.services(config.TWILIO_CHAT_SERVICE_SID)
.conversations(conversationSid)
.messages.create({ author, body });
};
Once you've done that, you need to one other thing. Because you add your participant to the conversation using the REST API, you don't need to join the conversation in the front-end. So you can remove the line
await conversation.join();
from src/twilio/index.ts.
One last thing, you can get better error messages in the front-end if you log out error.body rather than just error or error.message.
try {
const conversation = await client.getConversationByUniqueName(room);
if (message && String(message).trim()) {
await conversation.sendMessage(message);
}
} catch (error) {
console.log("error", error);
console.log("error body", error.body);
toast.error("Unable to create conversation, please reload this page");
}
Twilio have an official blog for Build a Chat App with Twilio Programmable Chat and React.js, please check it out once,
Here is the link - https://www.twilio.com/blog/build-a-chat-app-with-twilio-programmable-chat-and-react
Firebase denies permission after the first time to push data to realtime database... but then works when I push data on the second time...
export const writeClass = async (
courseId,
topicName,
classIntro,
youtubeLinkId,
classNoteLink
) => {
const dbRef = ref(getDatabase());
try {
const totalClasses = await get(
child(dbRef, `Courses/${courseId}/totalClasses`)
);
var totalClassesNum = parseInt(totalClasses.val()) + 1;
console.log(totalClasses.val());
const msg = await update(
ref(db, `Courses/${courseId}/classes/${totalClassesNum}`),
{
classIntro: classIntro,
topicName: topicName,
youtubeLinkId: youtubeLinkId,
classNoteLink: classNoteLink,
}
)
.then(() => {
update(ref(db, `Courses/${courseId}`), {
totalClasses: totalClassesNum,
});
console.log("Section added");
return true;
})
.catch((error) => {
console.log(error.message);
// alert(error.message)
return error.message;
});
return msg;
} catch (error) {
// alert(error.message)
console.log(error);
}
};
The firebase rules :
".write" :
"root.child('specialUsers').child(auth.uid).child('is_admin').val()
=== true
Firebase automatically restores the signed-in user when the page reloads/app restarts, but this requires it to call the server (to check a.o. whether the account was disabled) and that may take some time.
This means that the first time your code runs the user may not be signed in yet, and you need to check for that in your code. It's a bit unclear exactly where in your code the problem occurs, but you'll want to wrap that in:
if (firebase.auth().currentUser !== null) {
...
}
Even better would be to react to when the user is signed in or out, which you can do as shown in the first snippet in the documentation on getting the currently signed in user:
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
I have created a web app with Firebase and React.js and implemented sign-in with Google. I then tried to implement GoogleOneTapSignin and the one-tap-sign-in UI is working successfully because I used the react-google-one-tap-login npm package.
If may react app I have a function that listens for AuthStateChange and then either registers the user if they are new or sign in them if they are already a member and also updates the state if they logged. out.
Now that I have implemented google-one-tap-login, I was expecting the onAuthSTaetChanged function to be triggered if a user signs in using the google-one-tap-login but it is not the case.
Below is the part of my App.js code that handles the user auth.
const classes = useStyles();
const dispatch = useDispatch();
const alert = useSelector(state => state.notification.alert);
// Handling Google-one-tap-signin
useGoogleOneTapLogin({
onError: error => console.log(error),
onSuccess: response => {
console.log(response);
const credential = provider.credential(response);
auth.signInWithCredential(credential).then(result => {
const {
user
} = result;
console.log(user);
});
},
googleAccountConfigs: {
client_id: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
});
//Handling firebase authentification
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
// If there is a user create the user profile and update useState
if (user) {
// createUserProfile function creates the user profile in firestore if they are new
const userRef = await createUserProfileDocument(user);
userRef.onSnapshot(snapshot => {
const doc = snapshot.data();
dispatch(
setUser({
id: snapshot.id,
...doc
})
);
});
} else {
dispatch(setUser(null));
}
});
return () => {
unsubscribe();
};
}, [dispatch]);
I tried to implement the solution suggested by the 2nd answer in this StackOverflow question but I get the error below on the console. when I use google-one-tap-sign-in. Remember I am not using the FirebaseUi library. So far my application only uses the sign in with Google
t {
code: "auth/argument-error",
message: "credential failed: must provide the ID token and/or the access token.",
a: null
}
a: null
code: "auth/argument-error"
message: "credential failed: must provide the ID token and/or the access token."
The ID token required by Firebase's signInWithCredential function exists within the credential property of the response object. Here is a sample function below using Firebase V8.
// firebase V8
function handleCredentialResponse(response) {
if (response) {
const cred = auth.GoogleAuthProvider.credential(response.credential);
// Sign in with credential from the Google user.
return auth().signInWithCredential(cred);
}
}
Firebase v9
// firebase V9
import { getAuth, GoogleAuthProvider, signInWithCredential } from "firebase/auth";
const auth = getAuth();
function handleCredentialResponse(response) {
if (response) {
const cred = GoogleAuthProvider.credential(response.credential)
// Sign in with credential from the Google user.
return signInWithCredential(auth, cred);
}
}
The response param is a credential response returned from the Google one-tap function callback.
google?.accounts.id.initialize({
client_id: your-google-app-client-id.apps.googleusercontent.com,
callback: handleCredentialResponse,
});
google?.accounts.id.prompt((notification) => {
console.log(notification);
});
i can't figured out why when i call a method with provider, walletconnect keeps redirecting me in https://link.trustwallet.com instead of open me the app to approve the transaction. I've wrote the code for metamask and walletconnect and i used ethersjs to handle the provider.
Here's code for connection
const getBlockchain = (typew) =>
new Promise(async (resolve) => {
var provider;
if(typew == "metamask"){
/* #ts-ignore */
if(window.ethereum) {
/* #ts-ignore */
await window.ethereum.enable();
/* #ts-ignore */
provider = new ethers.providers.Web3Provider(window.ethereum);
}
}
if(typew == "walletconnect"){
providerEth = new WalletConnectProvider({
chainId: 56,
rpc: {
56: "https://bsc-dataseed.binance.org",
}
});
await providerEth.enable();
provider = new ethers.providers.Web3Provider(providerEth);
provider.off("disconnect");
provider.on("disconnect", () => {
closeBlockhain();
})
}
if(provider !== undefined){
const signer = provider.getSigner();
/* #ts-ignore */
const signerAddress:any = await signer.getAddress();
const contract = new Contract(
Contract.networks[56].address,
Contract.abi,
signer
);
const contract_two = new Contract(
Contract_two.networks[56].address,
Contract_two.abi,
signer
);
configConnections = {
connected: true,
provider,
contract,
contract_two,
signerAddress: signerAddress
}
resolve(configConnections);
}
resolve(configConnections);
});
Here's the code for method
let test = await configConnections.contract.awardItem(ethers.utils.formatBytes32String("1"),res.data.hash);
Can't explain to myself why if i open the app on mobile and try to call method, the browser redirects me on other page not on the TrustWallet or Metamask App.
Thank you.
I'm creating a global function that checks whether the jwt token is expired or not.
I call this function if I'm fetching data from the api to confirm the user but I'm getting the error that I cannot update during an existing state transition and I don't have a clue what it means.
I also notice the the if(Date.now() >= expiredTime) was the one whose causing the problem
const AuthConfig = () => {
const history = useHistory();
let token = JSON.parse(localStorage.getItem("user"))["token"];
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
localStorage.removeItem("user");
history.push("/login");
} else {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
};
I'm not sure if its correct but I call the function like this, since if jwt token is expired it redirect to the login page.
const config = AuthConfig()
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
I updated this peace of code and I could login to the application but when the jwt expires and it redirect to login using history.push I till get the same error. I tried using Redirect but its a little slow and I could still navigate in privateroutes before redirecting me to login
// old
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime)
// change
if (exp < Date.now() / 1000)
i would start from the beginning telling you that if this is a project that is going to production you always must put the auth token check in the backend especially if we talk about jwt authentication.
Otherwise if you have the strict necessity to put it in the React component i would suggest you to handle this with Promises doing something like this:
const config = Promise.all(AuthConfig()).then(()=> productData());
I would even consider to change the productData function to check if the data variable is not null before saving the state that is the reason why the compiler is giving you that error.
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
data && setProduct(data);
};
Finally consider putting this in the backend. Open another question if you need help on the backend too, i'll be glad to help you.
Have a nice day!
I'm still not sure how your code is used within a component context.
Currently your API and setProduct are called regardless whether AuthConfig() returns any value. During this time, you are also calling history.push(), which may be the reason why you encountered the error.
I can recommend you to check config for value before you try to call the API.
const config = AuthConfig()
if (config) {
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
}
I'm assuming that AuthConfig is a hook, since it contains a hook. And that it's consumed in a React component.
Raise the responsibility of redirecting to the consumer and try to express your logic as effects of their dependencies.
const useAuthConfig = ({ onExpire }) => {
let token = JSON.parse(localStorage.getItem("user"))["token"];
const [isExpired, setIsExpired] = useState(!token);
// Only callback once, when the expired flag turns on
useEffect(() => {
if (isExpired) onExpire();
}, [isExpired]);
// Check the token every render
// This doesn't really make sense cause the expired state
// will only update when the parent happens to update (which
// is arbitrary) but w/e
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
setIsExpired(true);
return null;
}
}
// Don't make a new reference to this object every time
const header = useMemo(() => !isExpired
? ({
headers: {
Authorization: `Bearer ${token}`,
},
})
: null, [isExpired, token]);
return header;
};
const Parent = () => {
const history = useHistory();
// Let the caller decide what to do on expiry,
// and let useAuthConfig just worry about the auth config
const config = useAuthConfig({
onExpire: () => {
localStorage.removeItem("user");
history.push("/login");
}
});
const productData = async (config) => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
useEffect(() => {
if (config) {
productData(config);
}
}, [config]);
};