firebase Stripe API failing on first payment process - javascript

I have successfully integrated stripe and firebase, using the Run Subscription Payments with Stripe.
Here's the flow:
When I click register, it brings me to the Stripe Page, using this function:
export async function createCheckoutSession(activtyStatus){
let user = firebase.auth().currentUser;
const checkoutSessionRef = firestore
.collection('customers')
.doc(userID)
.collection('checkout_sessions')
.add({
price: price,
success_url: "https://xxx/successPage",
cancel_url: "https://xxx/signin",
});
// Wait for the CheckoutSession to get attached by the extension
(await checkoutSessionRef).onSnapshot(function (snap) {
const { error, sessionId } = snap.data();
if (error) {
console.log(`An error occured: ${error.message}`);
}
if (sessionId) {
//live key
const stripe = window.Stripe('pk_livekeyxxxx');
stripe.redirectToCheckout({sessionId})
console.log("logged stripe")
}
});
}
If successful, it goes to to my SuccessPage, which just checks if the subscriptions collection was created in firebase or not.
so what happens is, if the payment is successful, it will just render /clients route, and if it isn't, it will redirect to createCheckoutSession function, which goes back to allow the customer to pay again. So when I enter the payment details on Stripe, SOMETIMES, it goes to clients and adds the subscription, and SOMETIMES, it redirects to the Stripe page, and makes me enter ti again, in which then it works.
is this a bug in my code, or is firebase and stripe just finicky?

It sounds like you might be losing your reference to your customer when they arrive at the success page, making it so that you can't inspect their new subscription.
Beyond options like authenticated sessions to load your user profile, you could consider setting up your success page to receive the Session ID, and optionally include your own customer ID. You should of course still ensure your customer is authenticated before showing any personal information, but this might help you to load the data.
Update: once you have a successful session, you can retrieve it from the API (or inspect your webhook data) to look at the subscription attribute (API ref) to find the created subscription id.

Related

Firebase authentication: sendEmailVerification automatically verifies the user's email

I'm starting to use Firebase Authentication in my Next.js app.
I've enabled Email/Password authentication method and I've created the user directly from the Firebase console. I don't want to allow user sign up yet.
I've seen that the user email is not verified by default, so I started implementing the logic to do so with the provided sendEmailVerification method, just pretty simple code within the event of a button click:
const handleClick = async () => {
if (!user) {
console.warn(`Shouldn't request a verification email if user is not logged in!.`)
return
}
await sendEmailVerification(user)
}
But surprisingly, after that, the user's email gets automatically verified, and no email is even sent.
What am I missing?

signInWithCustomToken firebase - user not staying logged in

I am building an app (Main App) that authenticates through a seperate app (Auth App). I am able to signInWithCustomToken but the auth state does not persist between client browser refreshes even though onAuthStateChanged runs with the user after signInWithCustomToken.
Below is the authentication flow:
Users open the Main App, and click a button to open a popup that displays the Auth App.
window.open(AUTH_URL, 'window', 'width=400,height=600');
On the Auth App users create a Firebase account with email and
password.
firebase.auth().signInWithEmailAndPassword(email, password)
The Auth App makes a request to the Auth App's server to
generate a custom token.
// client side code
const idToken = await firebase.auth().currentUser.getIdToken()
const token = api.generateTokenAPIRequest(idToken);
// server side code
const generateTokenAPIRequest = (idToken) => {
const { uid } = await admin.auth().verifyIdToken(idToken);
return await admin.auth().createCustomToken(uid);
};
This custom token is passed back to the Main App via a postMessage.
window.opener.postMessage({ token }, APP_URL);
window.close();
When the Main App receives the authentication message, it signs the user in with the custom token.
window.onmessage = (e: MessageEvent) => {
if (e.origin !== AUTH_URL) { return; }
const { idToken } = e.data;
if (!idToken) return;
firebase.auth().signInWithCustomToken(idToken)
};
I listen to firebase.auth().onAuthStateChanged. This function runs correctly with the new user account but does NOT run again when I refresh the page. It is as if the user is not being stored in the Main App's storage.
It appears this was caused by an unrelated bug where firebase.auth().signOut() was being called unexpectedly. However I wanted to call out a few items in case someone stumbles upon this.
Make sure your service account is the same one you are using on your authenticated app. You can generate an Admin service account through the Firebase Console.
The post message approach has problems where a hacker can grab the ID token, sign in with it and then do all the actions the users can do. Make sure you check the origin of the post message like I do above. There are additional measures that can probably be put in place. I believe this is how the typical "Sign in with Google" works.
There are approaches with sessions that have been posted on Medium. Namely https://dev.to/johncarroll/how-to-share-firebase-authentication-across-subdomains-1ka8 and https://dev.to/brianburton/cross-domain-firebase-authentication-a-simple-approach-337k
Supporting this use case is currently an open issue https://github.com/firebase/firebase-js-sdk/issues/2303
Happy coding!

Update user state in database after HTTP cloud function is fired - stripe checkout

I store users in firebase realtime database and stripe to handle payments. An authenticated user can click a button and will get redirected to Stripe Checkout. After making a payment, user gets redirected to success_url back in the app. Next I would like to update user Object in database - just save the information that the payment was successful and its id.
The problem: I don't know how to find a user which completed a payment, after redirection to success_url. I would like to update user's data on database after the payment.
The idea: I can save the payment_intent in the user profile, as this information is sent to the client side with a session. And then, after the payment completion I could search for the payment_intent and update the user which had this data. But is this a good approach? Is there any better way to find a user?
My code is based on Firebase Cloud Function HTTP Requests.
Following the Stripe guidelines to accept a payment, I create a session for a user which wants to make a payment:
export const payment = functions.https.onRequest((request, response) => {
cors(request, response, async () => {
response.set("Access-Control-Allow-Headers", "Content-Type");
response.set("Access-Control-Allow-Credentials", "true");
response.set("Access-Control-Allow-Origin", "*");
await stripe.checkout.sessions.create(
{
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Test"
},
unit_amount: 1000
},
quantity: 1
}
],
mode: "payment",
success_url: "https://example.com/success",
cancel_url: "https://example.com/cancel"
},
function(err: Error, session: any) {
response.send(session);
// the session is sent to the client and can be used to finalise a transaction
}
);
});
});
On the client side I use axios to call a payment cloud function request and afterwards I pass the sessionId to Stripe.redirectToCheckout which starts the checkout redirection:
let sessionId = "";
await axios.post("payment function url request")
.then(response => {
sessionId = response.data.id;
});
const stripe: any = await loadStripe("stripe key");
stripe.redirectToCheckout({
sessionId: sessionId
});
After the customer completes a payment on Stripe Checkout, they’re redirected to the URL that is specified as success_url, it is advised to run a webhook from stripe when the checkout is completed.
It allows to fire another cloud function, which sends a response to the client side about a successful payment response.json({ received: true }):
export const successfulPayment = functions.https.onRequest(
(request, response) => {
const sig = request.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
request.rawBody,
sig,
endpointSecret
);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the checkout.session.completed event
// Should I update the user data in the database here?
if (event.type === "checkout.session.completed") {
const session = event.data.object;
console.log(`Event passed: ${session}`);
}
// Return a response to acknowledge receipt of the event
response.json({ received: true });
// Or how do I use this response to then update the user from the client side?
}
);
I will really appreciate all the help and suggestions :)
Thanks for your question, kabugh!
Next I would like to update user Object in database - just save the information that the payment was successful and its id.
First, you likely do not want to use the success URL as a signal for storing successful payment. It's possible the customer closes their browser, the internet connection between your server and the customer drops out, or that the customer just navigates away after successful payment and before hitting the success URL. Instead, it's highly recommended to use webhooks, specifically listening for the checkout.session.completed event. From the success URL, you can retrieve the Checkout Session and show the status to the customer if they land there, but you don't want to use that for fulfillment or as the source of truth for the status of the payment.
I can save the payment_intent in the user profile, as this information is sent to the client side with a session. And then, after the payment completion I could search for the payment_intent and update the user which had this data. But is this a good approach? Is there any better way to find a user?
I believe you mean the Checkout Session ID, not the PaymentIntent ID here. The PaymentIntent ID will not be created until after the user is redirected. Yes, this is a good idea and a good practice to store a reference to the ID of the Checkout Session with the user profile somewhere in your db. You could also set a metadata value on the Checkout Session when you create it, with some reference to the user's ID in your system. That way when you receive the checkout.session.completed webhook event, you can look up the user by ID based on the ID stored in metadata or by the ID of the Checkout Session stored with the user profile as discussed earlier.
it is advised to run a webhook from stripe when the checkout is completed. It allows to fire another cloud function, which sends a response to the client side about a successful payment response.json({ received: true })
Note that the webhook event is a POST request from Stripe's servers directly to your server. It will not in any way render or send a response back to the customer's browser or client. The webhook response from your server goes back to Stripe and there is no concept of a shared session between those three parties. When sending response.json({ received: true }) as the result of a webhook handler, that is sent back to Stripe, not to the customer's browser.

How to verify a user's ID token in firestore using Javascript?

I am building a react native application and am using Firebase, more specifically firestore, in order to manage my data. My current objective is to implement an auto login feature on my app, where if the user exits the app, I want them to stay signed in, unless they manually hit the Sign Out button before exiting the app. Here is my current process of doing this:
When the user logs into the app, I sign them in by:
firebase.auth().signInWithEmailAndPassword(email, password).
I then get their idToken by:
let authIdToken = "";
firebase
.auth()
.currentUser.getIdToken(true)
.then(function (idToken) {
authIdToken = idToken
})
.catch(function (error) {
console.log(error)
});
I then want to save this token into the phone, so when the user opens the app again, I can fetch this token and check its validity. If it is valid, then I can log the user in using their idToken. In react native, I can do this by doing:
AsyncStorage.setItem(
"userData",
JSON.stringify({
token: token,
})
);
Now when the app loads up:
const startScreen = props => {
useEffect(() => {
const tryLogin = async () => {
const userData = await AsyncStorage.getItem("userData");
const transformedData = JSON.parse(userData);
const { token } = transformedData;
await firebase
.auth()
.verifyIdToken(token, true)
.then((payload) => {
console.log(true)
})
.catch((error) => {
if (error.code == "auth/id-token-revoked") {
// Token has been revoked. Inform the user to reauthenticate or signOut() the user.
console.log("revoked")
} else {
console.log("error")
}
});
};
tryLogin();
}, []);
The Issue: When I try to verify the token this way, I am met with the following error: firebase.auth().verifyIdToken is not a function.
I read through the documentation and am unsure of how else to verify this token using JS. How do I verify it? Let me know if my verification process is incorrect and how it should be done. I am new to using firestore and doing authentication in general and hope to learn how to do it the right way.
Another helpful note: This is how I am configuring my firestore: !firebase.apps.length ? firebase.initializeApp(firebaseConfig) : {};
Thanks!
I then want to save this token into the phone, so when the user opens the app again, I can fetch this token and check its validity.
This is completely unnecessary. Firebase Auth with persist the signed in user, and automatically refresh the token without you having to do anything. All you need to do is listen to when updates to the token are made available, and act on the new token as needed. You can establish an ID token listener using onIdTokenChanged as shown in the linked API documentation:
firebase.auth().onIdTokenChanged(function(user) {
if (user) {
// User is signed in or token was refreshed.
}
});
Once you have this token, you know that the user is successfully signed in. There is nothing left to do. There is no need to use it to sign in.
Also, you can't verify the token on the frontend. The verifyIdToken method you're looking at is for the Admin SDK only, which only runs on the backend. The idea is that you get the token on the fronend, then pass it to the backend as described in the documentation for the Admin SDK. The backend uses this to securely determine if the user on the frontend is who they say they are.
Since you didn't say if you have a backend or not, dealing with this token might not be necessary at all. If you just want to know when the user is signed in (even if they are just returning to the page after being away, then you can skip everything above and just use an auth state observer. Again, Firebase Auth persists information about the user so you don't have to sign them in again. The observer will tell you when the automatic sign-in is complete, or if they are not signed in at all.

How to send Facebook authentication details to Firebase using a Cordova plugin & Firebase template

Sorry this question is kind of long, it's because I've been trying to solve this problem for a while and want to make sure I don't leave any info out. I'm building a Cordova app and using Firebase for the authentication/database back end. I've been trying to authenticate users into Firebase using a Log in with Facebook button for almost a week now, but I haven't been able to get it to work.
Originally I tried following Firebase's example here: https://firebase.google.com/docs/auth/web/facebook-login (I need to use the "Advanced: Handle the sign in flow manually" as it is a Cordova Android & iOS app), this example didn't work for me as the link to Facebook's SDK script (//connect.facebook.net/en_US/sdk.js) kept throwing the error:
file://connect.facebook.net/en_US/sdk.js Failed to load resource: net::ERR_FILE_NOT_FOUND
I tried to fix this error in several ways, such as:
Changing it to https://connect.facebook.net/en_US/sdk.js (this resulted in the error: Can't Load URL: The domain of this URL isn't included in the app's domains. To be able to load this URL, add all domains and subdomains of your app to the App Domains field in your app settings. )
Added the links in question to the list of "Valid OAuth redirect URIs" and domains in the Facebook app settings
Storing the file in my local file system (and locally inside the app on my phone)
Including the entire SDK inside the head of my index.html file
None of these attempts worked. So instead I decided to use the plugin cordova-plugin-facebook from here: https://github.com/bisrael/cordova-plugin-facebook
This is the code I'm using to get the user's information from Facebook with the plugin:
function logInWithFacebook(){
CordovaFacebook.login({
onSuccess: function(result) {
console.log(result);
console.log(result.authToken);
// Store or send the user auth/access key here?
// Get user's name
retrieveUserDetails();
if(result.declined.length > 0) {
alert("The User declined something!");
}
},
onFailure: function(result) {
if(result.cancelled) {
alert("The user doesn't like my app");
} else if(result.error) {
alert("There was an error:" + result.errorLocalized);
}
}
});
}
function retrieveUserDetails(){
// Now that the user has authroised the app, make request to CordovaFacebook plugin to get user's name
CordovaFacebook.graphRequest({
path: '/me',
params: { fields: 'name' },
onSuccess: function (userData) {
console.log(userData);
console.log(userData.name);
// Here somehow send the retrieved username and send it to the Firebase function so that it's linked with the auth key.
},
onFailure: function (result) {
if (result.error) {
Error.log('error', 'There was an error in graph request:' + result.errorLocalized);
}
}
});
}
I'm now able to click on a log in button and log in successfully through Facebook. That process is returning a user auth/access key and the user's name from Facebook.
As I understand it, the manual log in flow example in Firebase's docs (https://firebase.google.com/docs/auth/web/facebook-login) takes the key returned from Facebook, converts it into a Firebase key, and then enters the user's newly created Firebase key and their username into Firebase's servers.
This seems pretty straight forward in the following sample code:
function checkLoginState(event) {
if (event.authResponse) {
// User is signed-in Facebook.
var unsubscribe = firebase.auth().onAuthStateChanged(function(firebaseUser) {
unsubscribe();
// Check if we are already signed-in Firebase with the correct user.
if (!isUserEqual(event.authResponse, firebaseUser)) {
// Build Firebase credential with the Facebook auth token.
var credential = firebase.auth.FacebookAuthProvider.credential(
event.authResponse.accessToken);
// Sign in with the credential from the Facebook user.
firebase.auth().signInWithCredential(credential).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
} else {
// User is already signed-in Firebase with the correct user.
}
});
} else {
// User is signed-out of Facebook.
firebase.auth().signOut();
}
}
function isUserEqual(facebookAuthResponse, firebaseUser) {
if (firebaseUser) {
var providerData = firebaseUser.providerData;
for (var i = 0; i < providerData.length; i++) {
if (providerData[i].providerId === firebase.auth.FacebookAuthProvider.PROVIDER_ID &&
providerData[i].uid === facebookAuthResponse.userID) {
// We don't need to re-auth the Firebase connection.
return true;
}
}
}
return false;
}
FB.Event.subscribe('auth.authResponseChange', checkLoginState);
My question is, how can I send the auth key and username returned from the Cordova plugin code, to Firebase's example code so that it works smoothly?
Firebase's example code includes this listener which listens for any change in the Facebook authorization status: FB.Event.subscribe('auth.authResponseChange', checkLoginState); but as this uses Facebook's SDK it won't work with my current set up.
I'm using the following Firebase chat app as a template to work from: https://gist.github.com/puf/8f67d3376d80ed2d02670d20bfc4ec7d as you can see it has a Login with Facebook button, but no code for handling the process, I'm trying to apply parts of the manual log in flow example in Firebase's docs (https://firebase.google.com/docs/auth/web/facebook-login) with data returned from the cordova-plugin-facebook queries, and integrate both with Firebase's chat app template.
I'm really at a loss as to what to do next, I've tried everything I can think of. Any help in solving this problem would be really, really appreciated.
Thank you in advance!
UPDATE
Questions and answers:
How does it work at the moment?
Right now I have a "Facebook Login" button - when this is clicked it runs logInWithFacebook(). This function uses the CordovaFacebook plugin, it also runs the function retrieveUserDetails() after the user signs in with Facebook. retrieveUserDetails() gets some user info from Facebook which I hope to then insert into my Firebase database.
logInWithFacebook() works correctly (it opens up a Facebook login page, and when the user logs in, I'm able to console.log the user's Facebook ID, and the Facebook access Token.
retrieveUserDetails() also works correctly (I'm able to console.log the user's name taken from Facebook).
How do you want it to work?
I'm happy with how the first half of the process is working (the logging in with Facebook and retrieving user details is working correctly). However I want this log in to trigger Firebase's auth state change listener, so that Firebase detects and confirms that the user has logged in:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
console.log("User is signed in.");
} else {
console.log("User is not signed in.");
}
});
What is not working the way you want?
The first half of the process is working correctly, but I'm lost when it comes to what to do with the accessToken returned from Facebook. From reading the docs I think that Firebase is supposed to convert this token into a Firebase access token, and then that is used to log the user into Firebase (this would also trigger the above AuthStateChanged function). From there I want to be able to insert any data I've retrieved from Facebook (the user's name etc) into my Firebase database. But the main problem is getting the Facebook accessToken converted into a Firebase login (the second block of code in my original question is where I'm trying to perform the conversion/sign into Firebase).
Because I'm using Cordova, this method (logging into Facebook with a plugin and then handling the conversion of the accessToken) seems to be the only way to log in with Facebook. But I'm totally lost on how to complete the second half.
UPDATE 2
I've trimmed parts from the sample convert-Facebook-token-to-Firebase-token code from the docs so that the Facebook SDK isn't required. And it appears to be working. This is the code after cutting away the SDK related parts:
// First, define the Facebook accessToken:
var FBaccessToken = result.accessToken;
// Build Firebase credential with the Facebook auth token.
var credential = firebase.auth.FacebookAuthProvider.credential(
FBaccessToken);
// Sign in with the credential from the Facebook user.
firebase.auth().signInWithCredential(credential).then(function(user){
console.log("It looks like we signed into Firebase with the Facebook token correctly.");
}, function(error) {
console.log("Something went wrong, user isn't signed into Firebase with the FB token.");
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
I still need to add the user's email from Facebook and try to send that while logging into Firebase too - so that I'll have some identifier for the user in the Firebase console, but this is a good start.
2nd UPDATE
The below code successfully gets user data from Facebook after the user authorizes the app:
CordovaFacebook.graphRequest({
path: '/me',
params: { fields: 'first_name,last_name,email,locale,gender,age_range,picture.width(200).height(200)' },
onSuccess: function (userData) {
console.log(userData)
var first_name = userData.first_name;
var last_name = userData.last_name;
var email = userData.email;
var locale = userData.locale;
var gender = userData.gender;
var min_age = userData.age_range.min;
var profile_picture = userData.picture.data.url;
// Enter user details into the Firebase database:
firebase.database().ref('users/' + uid).set({
first_name: first_name,
last_name: last_name,
email: email,
locale: locale,
gender: gender,
min_age: min_age,
profile_picture : profile_picture
});
console.log("Facebook user data should now be in the database!");
},
onFailure: function (result) {
if (result.error) {
Error.log('error', 'There was an error in graph request:' + result.errorLocalized);
}
}
});
(Just an answer to the last update, as you figured out the rest :))
How to get user email from CordovaFacebook.login()
Looking at the CordovaFacebook documentation you can add a permissions property on the object passed to the login method.
According to the Facebook API documentation the permission name for email is just "email".
I haven't tested, but I think this should work:
CordovaFacebook.login({
permissions: [ 'email' ],
onSuccess: function(result) {
console.log('email:', result.email);
...
},
onFailure: function(result) {
...
}
});

Categories