Em termos gerais, Sails.js é um gerador de códigos JavaScript que entrega, em poucos minutos, um protótipo executável de uma Aplicação WEB do tipo backend robusta, segura e escalável. O resultado é a capacidade de entregar páginas HTML5 + CSS + JavaScript dinâmicas ao Cliente Web.
O Stripe é um processador de pagamentos online que permite que empresas aceitem pagamentos seguros e escaláveis.
O Sails.js já vem com uma pre integração habilitada, mas é preciso expandir suas funcionalidades.
Se não está familiarizado com os pré-requisitos acima, recomendamos a leitura dos conteúdos abaixo antes de continuar:
/config/custom.js com as chaves de acesso ao Stripe.
/**************************************************************************
* *
* Billing & payments configuration *
* *
* (https://dashboard.stripe.com/account/apikeys) *
* *
**************************************************************************/
// stripePublishableKey: `stripePublishabelKey`,
// stripeSecret: `stripeSecret`,
//--------------------------------------------------------------------------
// /\ Configure these to enable support for billing features.
// || (Or if you don't need billing, feel free to remove them.)
//--------------------------------------------------------------------------
Ou o /env/production.js, se as configurações forem diferentes para cada
ambiente.
publishable key e secret
key.
/**************************************************************************
* *
* Billing & payments configuration *
* *
* (https://dashboard.stripe.com/account/apikeys) *
* *
**************************************************************************/
stripePublishableKey: `${process.env.STRIPE_PUBLISHABLE_KEY}`,
stripeSecret: `${process.env.STRIPE_SECRET}`,
//--------------------------------------------------------------------------
// /\ Configure these to enable support for billing features.
// || (Or if you don't need billing, feel free to remove them.)
//--------------------------------------------------------------------------
Não informe os valores obtidos diretamente no código-fonte! Isso será uma falha de segurança! Coloque-os como variáveis de ambiente!
/* signup.js, linha105 */
// If billing features are enabled, save a new customer entry in the Stripe API.
// Then persist the Stripe customer id in the database.
if (sails.config.custom.enableBillingFeatures) {
let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
emailAddress: newEmailAddress
}).timeout(5000).retry();
await User.updateOne({id: newUserRecord.id})
.set({
stripeCustomerId
});
}
As etapas de 1 a 3 são realizadas no sítio do stripe, então não vamos falar sobre isso aqui.
Crie uma "Action 2" para iniciar a sessão de pagamento.
/* stripe-sessions-create.js */
module.exports = {
friendlyName: 'Do stripe session',
description: '',
inputs: {
priceId: { // <-- conforme definido no site do Stripe
type: 'string',
required: true
},
quantity: { // <-- se for realizada a compra multipla
type: 'number',
defaultsTo: 1
},
recurring: { // <-- se for compra recorrente
type: 'boolean',
defaultsTo: false
}
},
exits: {},
fn: async function (inputs) {
const User = sails.models.user;
const user = await User.findOne({id: this.req.me.id});
const stripeCustomerId = user.stripeCustomerId;
if (!stripeCustomerId) {
return {}
}
// Nao esquecer do npm install stripe --save // Não esquecer de editar o arquivo config/custom.js //
const stripe = require('stripe')(sails.config.custom.stripeSecret);
try {
const stripeSession = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: inputs.recurring ? 'subscription' : 'payment',
success_url: sails.config.custom.baseUrl + '/account/stripe-checkout-session-successfully',
line_items: [
{
price: inputs.priceId,
quantity: inputs.quantity
}
]
}
)
return {stripeSession: stripeSession};
} catch (err) {
sails.log.error(err)
return {}
}
}
};
/* view-stripe-checkout-session-successfull.js */
module.exports = {
friendlyName: 'View stripe checkout session successfull',
description: 'Display "Stripe checkout session successfull" page.',
exits: {
success: {
viewTemplatePath: 'pages/account/stripe-checkout-session-successfull'
}
},
fn: async function () {
if (this.req?.me?.id) {
// Nao esquecer do npm install stripe --save // Não esquecer de editar o arquivo config/custom.js //
const stripe = require('stripe')(key);
const key = sails.config.custom.stripeSecret
const Checkout = sails.models.checkout; <-- Esta é uma tabela de banco de dados que armazena informações sobre pagamentos realizados através do Stripe.
const checkouts = await Checkout.find({user: this.req.me.id});
const items = []
const intents = []
for (let checkout of checkouts) {
const lineItems = await stripe.checkout.sessions.listLineItems(
checkout.stripeSessionId
);
items.push(...lineItems.data);
const paymentIntents = await stripe.paymentIntents.search({
query: `customer:"${checkout.customer}"`,
});
intents.push(...paymentIntents.data);
}
for (let i of items) {
const c = await Checkout.findOne({priceId: i.price.id})
if (c) {
i.stripeSessionId = c.stripeSessionId
i.payment_status = c.payment_status
i.paymentUrl = JSON.parse(c.content).url
i.claimedAt = c.claimedAt
i.customer = c.customer
}
}
return {items: items, intents: intents};
} else {
return {items: [], intents: []};
}
}
};
<div id="stripe-checkout-session-successfull" v-cloak>
<div class="container">
<div class="display-1">
Pagamento realizado com sucesso!
</div>
<div class="display-3">
{{stripeSession.currency}} {{stripeSession.amount_total}}
</div>
<label class="mt-2" for="compras">Suas compras...</label>
<ul id="compras" class="ul list-group">
<li class="list-group-item" v-for="(item, index) in items" :key="index">
{{index+1}}. {{item.description}} - {{item.quantity}}X {{item.price.currency.toUpperCase()}} {{getCurrency(item.price.unit_amount)}} ({{item.payment_status}})
<span class="border-1" v-if="item.payment_status==='unpaid'">
<a :href="item.paymentUrl"> [ Clique para pagar ]</a>
</span>
<span class="border-1" v-if="item.payment_status==='paid'">
<button class="btn btn-sm btn-primary" v-if="!!item.claimedAt">[Já ativado]... em {{item.claimedAt}}</button>
<button class="btn btn-sm btn-primary" v-if="!item.claimedAt" @click="activateIt(item)">[Ativar]</button>
</span>
<em class="ml-1 mr-1 fa fa-question-circle" :title="item.stripeSessionId"></em>
</li>
</ul>
<label class="mt-2" for="intents">Suas transações...</label>
<table class="table table-striped" id="intents">
<caption></caption>
<thead>
<tr>
<th id="1">Data</th>
<th id="2">Moeda</th>
<th id="3">Orçado</th>
<th id="4">Pago</th>
<th id="5">Ativo/Inativo</th>
<th id="6">Situação</th>
<th id="7"></th>
</tr>
</thead>
<tbody>
<tr v-for="item of intents">
<td headers="1">
<js-timestamp :at="item.created"></js-timestamp>
</td>
<td headers="2">{{item.currency.toUpperCase()}}</td>
<td headers="3">{{(item.amount)}}</td>
<td headers="4">{{(item.amount_received)}}</td>
<td headers="5">{{item.livemode?'Ativo':'Inativo'}}</td>
<td headers="6">{{item.status}}</td>
<td headers="7">
<em class="ml-1 mr-1 fa fa-question-circle" :title="item.payment_details.order_reference"></em>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="7"></td>
</tr>
</tfoot>
</table>
<modal v-if="loading" >
<div class="display-3">Carregando...</div>
</modal>
<modal v-if="confirm" @close="confirm=false">
<div class="display-3">Está certo de?</div>
<p class="text-center">Ativar o pacote {{instance.description}}</p>
<p>Ao pressionar [Sim] você estará ativando este pacote definitivamente.</p>
<button class="btn btn-lg btn-primary mb-1" @click="activate()">Sim</button>
<button class="btn btn-lg btn-primary" @click="confirm=false">Não</button>
</modal>
</div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>
/* stripe-checkout-session-successfull.page.js */
methods: {
...
activate: function () {
this.confirm = false
this.loading = true
Cloud
.processBonus
.with({stripeSessionId: this.instance.stripeSessionId, claimer: this.me.id})
.then((res) => {
if(res.processed){
this.loading = false
alert('Bonus claimed successfully! Check your account or Bunker`s inventory')
window.location.reload()
} else {
console.log(res.error)
this.loading = false
alert('Sorry! We have an error on processing data')
}
})
.catch(err => {
console.log(err)
this.loading = false
alert('Sorry! We have an error on processing data')
})
}
...
}
/* process-bonus.js */
module.exports = {
friendlyName: 'Process bonus',
description: '',
inputs: {
stripeSessionId: {type: 'string', required: true, example: 'cs_test_123'},
claimer: {type: 'string', description: 'The id of user who claim the bonus', required: true}
},
exits: {},
fn: async function (inputs) {
const Checkout = sails.models.checkout;
const Bonus = sails.models.bonus;
const User = sails.models.user;
const checkout = await Checkout.findOne({stripeSessionId: inputs.stripeSessionId})
if (!checkout || !!checkout.claimedAt) return {processed: false}
const bonus = await Bonus.findOne({stripePriceId: checkout.priceId})
if (!bonus) return {processed: false};
const user = await User.findOne({id: inputs.claimer})
if (!user) return {processed: false};
const tipo = bonus.tipe;
// Example: ITEM_UPLINK_1, ITEM_ANTENA_2 --> Add 1/2 antennas into Users` Bunker`s Inventory
if (tipo.startsWith('ITEM_')) {
const parts = tipo.split('_');
const itemId = parts[1];
const quantity = parts[2];
const Item = sails.models.item;
try {
const user = await User.findOne({id: inputs.claimer}).populate('bunker')
const Inventory = sails.models.inventory;
const inventory = await Inventory.find({id: user.bunker.id}).limit(1)
if (!inventory || inventory.length === 0) return {processed: false}
for (let i = 0; i < quantity; i++) {
await Item.create({
name: `PURCHASED ${itemId}`,
value: (++i)+'',
tipe: itemId,
creator: user.id,
inventory: inventory[0].id
});
}
await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
return {processed: true}
} catch (err) {
sails.log.error(err);
return {processed: false, error: err};
}
}
// Example: POINT_CRAFT_100 -> Plus 100 Craft Points to Claimer
if (tipo.startsWith('POINT_')) {
const parts = tipo.split('_');
const itemId = parts[1];
const quantity = parts[2];
const userPoints = user.level;
if (itemId === 'CRAFT') {
try {
await User.updateOne({id: user.id}).set({level: userPoints + parseInt(quantity)});
await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
return {processed: true}
} catch (err) {
sails.log.error(err);
return {processed: false, error: err};
}
}
}
// Example: SUBSCRIPTION_YEARS_1, SUBSCRIPTION_MONTHS_3, SUBSCRIPTION_DAYS_30
if (tipo.startsWith('SUBSCRIPTION_')) {
const parts = tipo.split('_');
const itemId = parts[1];
const quantity = parseInt(parts[2], 10);
const today = new Date();
let timestampe;
if (itemId === 'YEARS') {
today.setFullYear(today.getFullYear() + quantity);
timestampe = today.getTime();
}
if (itemId === 'MONTHS') {
today.setMonth(today.getMonth() + quantity);
timestampe = today.getTime();
}
if (itemId === 'DAYS') {
today.setDate(today.getDate() + quantity);
timestampe = today.getTime();
}
if (!timestampe) return {processed: false, error: 'Invalid subscription type'};
try {
await User.updateOne({id: user.id}).set({subscriptionUntil: timestampe});
await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
return {processed: true};
} catch (err) {
sails.log.error(err);
return {processed: false, error: err};
}
}
return {processed: false, error: 'Invalid bonus type'};
}
};
/* stripe-checkout-session-successfull.page.js */
...
methods: {
...
retrieveStripeCheckout: function (stripeSessionId) {
this.loading = true
Cloud
.retrieveStripeCheckout
.with({sessionId: stripeSessionId})
.then(() => {
window.location.reload()
})
.catch(err => {
console.log(err)
this.loading = false
alert('Sorry! We have an error on refreshing data')
})
},
...
/* qualquer *.page.js */
methods: {
...
runStripeCheckout: function(priceId, tipe, userId) {
Cloud.doStripeCheckout.with({priceId: priceId, quantity: 1, recurring: tipe==='recurring'}).then(function(response) {
if(response.stripeSession){
Cloud
.saveStripeCheckout // <-- Cria o registro de Checkout realizado no banco de dados
.with({
checkout: response.stripeSession, // <-- stripeSession é retornado pelo Cloud.doStripeCheckout
priceId: priceId,
userId: userId
})
.then(()=>{
window.location.href=response.stripeSession.url
})
.catch((err)=>{console.log(err); alert("Sorry! Error on saving checkout session.")})
}else{
alert("Sorry! Error on creating checkout session.")
}
})
},
/* do-stripe-checkout.js */
module.exports = {
friendlyName: 'Do stripe checkout',
description: '',
inputs: {
priceId: {
type: 'string',
required: true
},
quantity: {
type: 'number',
defaultsTo: 1
},
recurring: {
type: 'boolean',
defaultsTo: false
}
},
exits: {},
fn: async function (inputs) {
const User = sails.models.user;
const user = await User.findOne({id: this.req.me.id});
const stripeCustomerId = user.stripeCustomerId;
if (!stripeCustomerId) {
return {}
}
const stripe = require('stripe')(sails.config.custom.stripeSecret);
try {
const stripeSession = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: inputs.recurring ? 'subscription' : 'payment',
success_url: sails.config.custom.baseUrl + '/account/stripe-checkout-successfully',
line_items: [
{
price: inputs.priceId,
quantity: inputs.quantity
}
]
}
)
return {stripeSession: stripeSession};
} catch (err) {
sails.log.error(err)
return {}
}
}
};
/* save-stripe-checkout.js */
module.exports = {
friendlyName: 'Save stripe checkout',
description: '',
inputs: {
checkout: {
type: 'json'
},
userId: {
type: 'string'
},
priceId: {
type: 'string'
}
},
exits: {
},
fn: async function (inputs) {
const Checkout = sails.models.checkout;
inputs.checkout.stripeSessionId = inputs.checkout.id
delete inputs.checkout.id
const stringVersion = JSON.stringify(inputs.checkout);
const converted = stringVersion.replaceAll('null',`""`)
const parsed = JSON.parse(converted);
return await Checkout.create({
stripeSessionId: parsed.stripeSessionId,
priceId: inputs.priceId,
object: parsed.object,
amount_subtotal: parsed.amount_subtotal,
amount_total: parsed.amount_total,
payment_status: parsed.payment_status,
customer: parsed.customer,
user: inputs.userId,
content: parsed
}).fetch();
}
};
/* retrieve-stripe-checkout.js */
module.exports = {
friendlyName: 'Retrieve stripe checkout',
description: '',
inputs: {
sessionId: { type: 'string', required: true }
},
exits: {
},
fn: async function (inputs) {
const key = sails.config.custom.stripeSecret
const stripe = require('stripe')(key);
const Checkout = sails.models.checkout; <-- Esta é uma tabela de banco de dados que armazena informações sobre pagamentos realizados através do Stripe.
const checkouts = await Checkout.find();
for(let checkout of checkouts){
const content = JSON.parse(checkout.content);
const paymentIntents = await stripe.paymentIntents.search({
query: `customer:"${content.customer}"`,
});
for(let paymentIntent of paymentIntents.data){
const paid = paymentIntent.status === 'succeeded';
if(paid){
await Checkout.update({stripeSessionId: checkout.stripeSessionId, payment_status: 'unpaid'}, {payment_status: 'paid'})
}
}
}
const lineItems = await stripe.checkout.sessions.listLineItems(
inputs.sessionId
);
return {lineItems: lineItems.data};
}
};