Stripe Payment of Course using FireStore Cloud Function - node.js

So I have made a page where students will get redirected to after choosing in which course they want to register themselves and on that page the students will provide their personal information and right after they'll click on 'continue for registration' button a stripe checkout form will open that will charge them the amount of that particular course and only then those students will get registered in the cloud firestor database collection. So i started learning from these tutorials link. However my cloud functions are written in NodeJS backend so i looked for some sample stripe apis for payment and found these sample apis on this github repo.
I am not sure how to hit these apis or if these are the right apis for me because my other functions are a little bit different then these stripe functions.
I would very much appreciate your help to know how can i implement this functionality. I am pasting the stripe fucntions here too.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const logging = require('#google-cloud/logging')();
const stripe = require('stripe')(functions.config().stripe.token);
const currency = functions.config().stripe.currency || 'USD';
// [START chargecustomer]
// Charge the Stripe customer whenever an amount is created in Cloud Firestore
exports.createStripeCharge = functions.firestore.document('stripe_customers/{userId}/charges/{id}').onCreate(async (snap, context) => {
const val = snap.data();
try {
// Look up the Stripe customer id written in createStripeCustomer
const snapshot = await admin.firestore().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 charge = {amount, currency, customer};
if (val.source !== null) {
charge.source = val.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
console.log(error);
await snap.ref.set({error: userFacingMessage(error)}, { merge: true });
return reportError(error, {user: context.params.userId});
}
});
// [END chargecustomer]]
// When a user is created, register them with Stripe
exports.createStripeCustomer = functions.auth.user().onCreate(async (user) => {
const customer = await stripe.customers.create({email: user.email});
return admin.firestore().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
exports.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 admin.firestore().collection('stripe_customers').doc(context.params.userId).get();
const customer = snapshot.data().customer_id;
const response = await stripe.customers.createSource(customer, {source: token});
return admin.firestore().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
exports.cleanupUser = functions.auth.user().onDelete(async (user) => {
const snapshot = await admin.firestore().collection('stripe_customers').doc(user.uid).get();
const customer = snapshot.data();
await stripe.customers.del(customer.customer_id);
return admin.firestore().collection('stripe_customers').doc(user.uid).delete();
});
Angular Test Component(Not Sure about this)
import * as firebase from 'firebase';
import { Component, Input, OnInit, ViewChild } from '#angular/core';
import { AngularFireFunctions } from '#angular/fire/functions';
import { PaymentService } from './payment.service';
import { environment } from '../../../../environments/environment';
#Component({
selector: 'ngx-payment-request',
templateUrl: './payment-request.component.html',
styleUrls: ['./payment-request.component.scss'],
providers: [PaymentService]
})
export class PaymentRequestComponent implements OnInit {
// #Input() amount: 100;
// #Input() label: 'Course';
elements: any;
paymentRequest: any;
prButton: any;
handler: any;
amount: number = 500;
confirmation: any;
loading = false;
#ViewChild('payElement', { static: true }) payElement;
constructor(private pmt: PaymentService, private functions: AngularFireFunctions) { }
ngOnInit() {
// this.loadStripe();
// this.pmt.showId();
this.pmt.showId();
this.handler = StripeCheckout.configure({
// key: environment.stripeKey,
image: 'https://oc1.ocstatic.com/images/logo_small.png',
locale: 'auto',
token: token => {
this.pmt.processPayment(token, this.amount)
},
source: async (source) => {
this.loading = true;
const user = this.pmt.showId();
firebase.functions().useFunctionsEmulator('hxxxxxx.net')
const fun = this.functions.httpsCallable('stripeCreateCharge');
this.confirmation = await fun({ source: source.id, amount: this.amount}).toPromise();
this.loading = false;
}
});
}
handlePayment(e) {
const user = this.pmt.showId();
this.handler.open({
name: 'FireStarter',
description: 'Pay your Dues',
amount : this.amount,
});
e.preventDefault();
}
}

Related

Load confirmation URL in Embedded app itself in Shopify

I have an embedded app in shopify which is an paid app ,Once user approves the billing ,i want the app to show the confirmation url in the embedded app itself instead it loads externally.
getsubscriptionurl.js
export const getSubscriptionUrl = async (ctx, shop) => {
const { client } = ctx;
console.log(`process.env.HOST - ${process.env.HOST}`);
console.log(`shop - ${shop}`);
console.log(`${process.env.HOST}/?shop=${shop}`);
const confirmationUrl = await client
.mutate({
mutation: RECURRING_CREATE(),
variables: {
returnUrl: `www.abc.com`,
}
})
.then(response => response.data.appSubscriptionCreate.confirmationUrl);
console.log("me "+ confirmationUrl);
return ctx.redirect(confirmationUrl);
};
server.js
app.prepare().then(async () => {
const server = new Koa();
const router = new Router();
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
const host = ctx.query.host;
ACTIVE_SHOPIFY_SHOPS[shop] = {scope:scope,accessToken:accessToken};
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
if (!response.success) {
console.log(
`Failed to register APP_UNINSTALLED webhook: ${response.result}`
);
}
// Redirect to app with shop parameter upon auth
// ctx.redirect(`/?shop=${shop}&host=${host}`);
server.context.client = await handlers.createClient(shop, accessToken);
await handlers.getSubscriptionUrl(ctx, shop);
},
})
);
You can't basically show the confirmation URL in your app, Shopify won't trust app developers to take sensitive info like payment details, so must open the confirmation URL into a new tab, where the merchant is viewing a Shopify payment page(made by shopify) that contains the payment details to be entered and on confirm the page will redirect the merchant to the return URL as you specified before.
For testing purposes
you can send a test param within the query to allow you to test without entering any payment details
const CREATE_SUB_MUTATION_RECURRING_ONLY = gql`
mutation RecurringSubscription(
$returnUrl: URL!
$test: Boolean!
$planName: String!
$amount: Decimal!
) {
appSubscriptionCreate(
test: $test
name: $planName
returnUrl: $returnUrl
lineItems: [
{
plan: {
appRecurringPricingDetails: {
price: { amount: $amount, currencyCode: USD }
interval: EVERY_30_DAYS
}
}
}
]
) {
userErrors {
field
message
}
confirmationUrl
appSubscription {
id,
currentPeriodEnd
}
}
}
`;
Now to test just pass true to test
result = await graphQlClient?.mutate({
mutation: CREATE_SUB_MUTATION_RECURRING_ONLY,
variables: {
returnUrl,
test,
planName: PLANS_DATA[planName].planName,
amount: PLANS_DATA[planName].price,
},
});

How can I remove a product as a seller and not have it appear in cart

Right now we are able to remove a product. When we remove a product we want the CART to reflect the changes. Meaning that if a user adds an item to CART and then the seller deletes the product for sale, the user will no longer see the product in cart.
Our MongoDB database is set up in this way:
Here's our code so far:
//DELETE PRODUCT
async function deleteProductInDb(uri_, seller_info) {
try {
const client = await MongoClient.connect(uri_, {
useUnifiedTopology: true, serverApi: ServerApiVersion.v1
});
const db = client.db("boreal_db");
var products_tb = db.collection("products");
const response = await products_tb.deleteOne({"_id": new ObjectID(seller_info._id)},{
})
client.close();
return response;
} catch(error) {
client.close();
console.log(error);
}
}
//DELETE PRODUCT
app.delete('/deleteProduct', function(req, res) {
console.log(req.body);
res.set({
'Access-Control-Allow-Origin': '*'
})
deleteProductInDb(uri, req.body).then(response => {console.log(response); res.send(response)});
});
// Custom useContext for the Cart
import React, { useReducer, useContext, createContext } from "react";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
/*
Manage the state cart (array).
ADD: Add items to cart array
REMOVE: Remove item from cart array using index
Throws an error if different case.
*/
const reducer = (state, action) => {
switch (action.type) {
case "ADD":
return [...state, action.item];
case "REMOVE":
const newArr = [...state];
newArr.splice(action.index, 1);
return newArr;
case "REMOVEALL":
return [];
default:
throw new Error(`unknown action ${action.type}`);
}
};
/* Context could be imported everywhere in the application
Implemented as an array (list), with each item assigned a key.
*/
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, []);
return (
<CartDispatchContext.Provider value={dispatch}>
<CartStateContext.Provider value={state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCart = () => useContext(CartStateContext);
export const useDispatchCart = () => useContext(CartDispatchContext);

"Can't Buy With This Account" POST Request Error When Using The Buys Endpoint To Buy Bitcoin

My goal is to try and get a Node app (written in Typescript) to buy bitcoin using my "GBP Wallet" (fiat account). I am currently trying this via using the Axios library, rather than any unofficial Coinbase client libraries out there. I am successfully able to get data that I need from the GET endpoints of the Coinbase API. However, with the following POST endpoint I get an error:
POST https://api.coinbase.com/v2/accounts/:account_id/buys
The error:
[ { id: 'invalid_request', message: "Can't buy with this account" } ]
Here is a simplified example of the code I am trying to run at the moment:
import * as crypto from 'crypto';
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import * as dotenv from 'dotenv';
dotenv.config();
const timer = (ms: number) => new Promise(res => setTimeout(res, ms));
function isNumeric(str: string): boolean {
const noSpacesStr = str.replace(/\s/g, '');
return !isNaN(Number(noSpacesStr)) && noSpacesStr.length !== 0;
}
async function coinbaseApiRequest(url: string, method: Method, data?: any): Promise<AxiosResponse> {
if (process.env.COINBASE_API_KEY === undefined || process.env.COINBASE_API_SECRET === undefined) {
throw new Error('Missing credentials');
}
const stringData = JSON.stringify(data);
const timestamp = Math.floor(Date.now() / 1000);
const message = data === undefined ? timestamp.toString() + method + url : timestamp.toString() + method + url + stringData;
const signature = crypto.createHmac("sha256", process.env.COINBASE_API_SECRET).update(message).digest("hex");
const config: AxiosRequestConfig = {
method,
url: `https://api.coinbase.com${url}`,
headers: {
'CB-ACCESS-SIGN': signature,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': process.env.COINBASE_API_KEY,
'CB-VERSION': '2021-05-19',
'Content-Type': 'application/json'
},
data
};
return axios(config);
}
async function getBuyPrice(): Promise<AxiosResponse> {
return coinbaseApiRequest('/v2/prices/BTC-GBP/buy', 'GET');
}
async function getSellPrice(): Promise<AxiosResponse> {
return coinbaseApiRequest('/v2/prices/BTC-GBP/sell', 'GET');
}
async function getGBPAccount(): Promise<AxiosResponse> {
return coinbaseApiRequest('/v2/accounts/GBP', 'GET');
}
async function getBitcoinAccount(): Promise<AxiosResponse> {
return coinbaseApiRequest('/v2/accounts/BTC', 'GET');
}
async function getGBPWalletPaymentMethodID(): Promise<string> {
const paymentMethods = await coinbaseApiRequest('/v2/payment-methods', 'GET');
for (let i = 0; i < paymentMethods.data.data.length; i++) {
const paymentMethod = paymentMethods.data.data[i];
if (paymentMethod.name === 'GBP Wallet' && paymentMethod.type === 'fiat_account') {
return paymentMethod.id;
}
}
throw new Error('GBP wallet has not been found');
}
async function postBuyBitcoin(accountId: string, amount: string, paymentMethod: string): Promise<AxiosResponse> {
if (!isNumeric(amount)) { throw new Error('amount arg is not a valid number'); }
const body = { amount, currency: 'BTC', payment_method: paymentMethod };
return coinbaseApiRequest(`/v2/accounts/${accountId}/buys`, 'POST', body);
}
async function run(): Promise<void> {
try {
const oldGBPAccRes = await getGBPAccount();
const oldBTCAccRes = await getBitcoinAccount();
const paymentMethodID = await getGBPWalletPaymentMethodID();
const buyRes = await postBuyBitcoin(oldGBPAccRes.data.data.id, '0.00001', paymentMethodID);
const newGBPAccRes = await getGBPAccount();
const newBTCAccRes = await getBitcoinAccount();
console.log(`old GBP account response:`);
console.log(oldGBPAccRes.data);
console.log(`old BTC account response:`);
console.log(oldBTCAccRes.data);
console.log(`bitcoin buy response:`);
console.log(buyRes);
console.log(`new GBP account response:`);
console.log(newGBPAccRes.data);
console.log(`new BTC account response:`);
console.log(newBTCAccRes.data);
console.log('Finished payment');
} catch (error: any | AxiosError) {
if (axios.isAxiosError(error)) {
console.log(error.response?.data.errors);
}
}
}
run();
All the GET request functions seem to work fine and return the responses correctly when I run them individually. The only issue I have is with the POST request function. I get the following error logged when running the code above:
[ { id: 'invalid_request', message: "Can't buy with this account" } ]
Not sure why this is happening, and there does not seem to be much on this online relevant to node based implementations, but this seems to be somewhat related. would appreciate some guidance. Thanks in advance.

How to send data from react editor to server?

I am trying to create an editor to update my backend data but I am stuck at sending data from client to backend
Here is my following front-end code:
import React, { useState } from "react";
import dynamic from "next/dynamic";
import { convertToRaw, EditorState, getDefaultKeyBinding } from "draft-js";
import draftToHtml from "draftjs-to-html";
const Editor = dynamic(
() => import("react-draft-wysiwyg").then((mod) => mod.Editor),
{ ssr: false }
);
const Missions = ({ teamData, editable }) => {
const { title, mission, teamId } = teamData;
const classes = useStyle();
const [missionContent, setMissionContent] = useState(mission);
const [editing, setEditing] = useState(false);
const [editorState, updateEditorState] = useState(EditorState.createEmpty());
const onEditorStateChange = (editData) => {
updateEditorState(editData);
};
const handleSave = async () => {
const selection = editorState.getSelection();
const key = selection.getAnchorKey();
const content = editorState.getCurrentContent();
const block = content.getBlockForKey(key);
const type = block.getType();
if (type !== "unordered-list-item" && type !== "ordered-list-item") {
if (
editorState.getCurrentContent().getPlainText("").trim().length !== 0
) {
const content = editorState?.getCurrentContent();
let html = await draftToHtml(convertToRaw(content));
await updateEditorState(EditorState.createEmpty(""));
setMissionContent(html.trim());
}
}
setEditing(false);
};
return (
<div className="team-mission-editor-container">
<Editor
wrapperClassName={"mission-editor-wapper"}
toolbarClassName={"mission-editor-toolbar"}
editorClassName={"mission-editor-editor"}
editorState={editorState}
onEditorStateChange={onEditorStateChange}
toolbar={{...}}
/>
)
Here is my back-end router:
router.put(
"/team/:teamId",
restrictedRoute,
checkData,
catchErrors(checkTeamPermissions),
catchErrors(updateTeamData)
);
and here is my update function from backend:
exports.updateTeamData = async (req, res) => {
// Get userId
const userId = req.session.passport.user.id;
// Get teamId
const publicTeamId = req.params.teamId;
// Fetch private id for team
const teamId = await getTeamId(publicTeamId);
// The user making the request
const userPublicId = req.session.passport.user.publicId;
// The creator of the team
const creatorPublicId = req.body.creator;
// Check who is making the request
if (userPublicId !== creatorPublicId) {
res.status(401).json("msg: You cant update a team you did not create");
}
// Updates
const payload = {
title: req.body.title,
mission: req.body.mission,
inputs: req.body.inputs,
outputs: req.body.outputs,
duration_in_months: req.body.duration_in_months,
status: req.body.status,
mergedTo: teamId,
};
// Update team data
await models.Team.update(payload, {
where: {
id: teamId,
creatorId: userId,
},
});
res.status(200).json("msg: Updated team successfully");
};
How can I send data fromo my editor to backend and update it?
Thank you so much for helping me

Docusign send envelope from template not showing tabs (node)

I'm trying to send an envelope from a template in a webhook listener. I'm using node.js with .mjs files. When I send the envelope via the docusign dashboard it has the tabs - full name, SSN, phone number, address. But when the API sends the envelope, it has those words but no fields next to them. This is a problem because we need that info and there's nowhere for the signer to input it. What could be causing the tabs to not appear when the envelope is sent from the api?
Here's the code to create an envelope and use a template (based off docs):
import docusign from 'docusign-esign';
export function makeEnvelope(args){
// Create the envelope definition
let env = new docusign.EnvelopeDefinition();
env.templateId = args.templateId;
// Create template role elements to connect the signer and cc recipients
// to the template
// We're setting the parameters via the object creation
let signer1 = docusign.TemplateRole.constructFromObject({
email: args.signerEmail,
name: args.signerName,
roleName: 'signer'});
// Create a cc template role.
// We're setting the parameters via setters
let cc1 = new docusign.TemplateRole();
cc1.email = args.ccEmail;
cc1.name = args.ccName;
cc1.roleName = 'cc';
// Add the TemplateRole objects to the envelope object
env.templateRoles = [signer1, cc1];
env.status = 'sent'; // We want the envelope to be sent
return env;
}
export async function useTemplate(args) {
let dsApiClient = new docusign.ApiClient();
dsApiClient.setBasePath(args.basePath);
dsApiClient.addDefaultHeader('Authorization', 'Bearer ' + args.accessToken);
let envelopesApi = new docusign.EnvelopesApi(dsApiClient);
// Make the envelope request body
let envelope = makeEnvelope(args.envelopeArgs);
// Call Envelopes::create API method
// Exceptions will be caught by the calling function
let results = await envelopesApi.createEnvelope(
args.accountId, {envelopeDefinition: envelope});
return results;
};
Here's the code for the webhook listener where I call useTemplate (please excuse the commented out code and the console logs - I'm still in the midst of figuring it all out):
import express from 'express';
import { useTemplate } from '../request/docusign/docusign-methods.mjs';
import opportunities from '../request/prosperworks/opportunities.mjs';
import people from '../request/prosperworks/people.js';
import customFields from '../request/prosperworks/custom-fields.mjs';
import { findTemplateIdByCohortName } from '../request/docusign/templates.mjs';
import { findSentEnvelopesByStudentEmail, voidEnvelope, findTemplateFromEnvelopeTemplateUri } from '../request/docusign/envelopes.mjs';
import { createJWT, getAccessTokenFromJWT } from '../request/docusign/token.mjs';
import { getAccessToken } from '../request/quickbooks/tokens.mjs';
import { findCustomerByEmail } from '../request/quickbooks/customer.mjs';
import { createCustomer } from '../request/quickbooks/customer.mjs';
import { createInvoice, sendInvoice } from '../request/quickbooks/invoice.mjs';
import { findItemByName } from '../request/quickbooks/item.mjs';
const router = express.Router();
export default router
.post('/copper/opportunity/updated', express.json(), async (req, res, next) => {
const { body } = req;
console.log('webhook received', body);
if(!Object.keys(body.updated_attributes).length) return res.send('irrelevant webhook');
const cohortChanged = !!body.updated_attributes?.custom_fields?.['94620']
console.log('cohort changed?', cohortChanged);
const interviewScheduledToAccepted = !!(body.updated_attributes?.stage?.[0] === 'Interview Scheduled' && body.updated_attributes?.stage?.[1] === 'Accepted')
console.log('interview scheduled to accepted?', interviewScheduledToAccepted);
const fullConditional = cohortChanged || interviewScheduledToAccepted;
console.log('full conditional', fullConditional);
if(fullConditional) {
try {
const jwt = await createJWT();
const docusign_access_token = await getAccessTokenFromJWT(jwt);
const opportunity = await opportunities.get(body.ids[0]);
const cohortId = opportunity?.custom_fields?.find(field => field.custom_field_definition_id === 94620)?.value || null;
const cohortName = customFields.getCohortNameById(cohortId);
console.log('cohort name', cohortName);
const templateId = await findTemplateIdByCohortName(cohortName, docusign_access_token);
const person = await people.findById(opportunity.primary_contact_id);
const email = person.emails[0].email;
console.log('email', email);
const { name } = person;
// if(interviewScheduledToAccepted) {
// const quickbooks_access_token = await getAccessToken();
// let customer = await findCustomerByEmail(email, quickbooks_access_token);
// if(customer === null) {
// customer = await createCustomer(cohortName, person, quickbooks_access_token);
// };
// console.log('customer', customer);
// const product = await findItemByName('Deposit', quickbooks_access_token);
// const invoice = await createInvoice(customer, product, cohortName, quickbooks_access_token);
// const sentInvoice = await sendInvoice(email, invoice.Invoice.Id, quickbooks_access_token)
// console.log('sent invoice', sentInvoice);
// }
const sentEnvelopes = await findSentEnvelopesByStudentEmail(email, docusign_access_token);
await Promise.all(
sentEnvelopes.filter(envelope => {
return envelope.emailSubject.includes('Enrollment Agreement');
})
.map(envelope => {
if(envelope.status === 'sent') return voidEnvelope(envelope.envelopeId, docusign_access_token);
})
);
const sentEnvelopesTemplates = await Promise.all(
sentEnvelopes
.filter(envelope => {
return envelope.status !== 'voided'
})
.map(envelope => {
return findTemplateFromEnvelopeTemplateUri(envelope.templatesUri, docusign_access_token);
})
);
const templateAlreadyUsedCheck = sentEnvelopesTemplates.reduce((outerAcc, templateArr) => {
if(templateArr.reduce((innerAcc, template) => {
if(template.templateId === templateId) innerAcc=true;
return innerAcc;
}, false)) {
outerAcc=true;
}
return outerAcc;
}, false);
if(templateAlreadyUsedCheck) return res.send('envelope already sent');
const envelopeArgs = {
templateId: templateId,
signerEmail: email,
signerName: name
}
console.log(envelopeArgs);
const envelope = await useTemplate({
basePath: process.env.DOCUSIGN_BASE_PATH,
accessToken: docusign_access_token,
accountId: process.env.DOCUSIGN_ACCOUNT_ID,
envelopeArgs
});
return res.send(envelope);
}
catch(err) {
console.log(err.message);
next(err);
}
} else {
return res.send('irrelevant webhook');
}
});
Inbar Gazit figured it out - it turns out I had student as the role name in the template and signer as the role name in my code so that's why it wasn't working.

Resources