houdini/client/js/nonprofits/donate/wizard.js

207 lines
7.7 KiB
JavaScript
Raw Normal View History

// License: LGPL-3.0-or-later
const flyd = require('flyd')
const R = require('ramda')
const h = require('snabbdom/h')
const url = require('url')
const render = require('ff-core/render')
const wizard = require('ff-core/wizard')
const scanMerge = require('flyd/module/scanmerge')
flyd.mergeAll = require('flyd/module/mergeall')
flyd.flatMap = require('flyd/module/flatmap')
flyd.zip = require('flyd-zip')
const getParams = require('./get-params')
const paymentStep = require('./payment-step')
const amountStep = require('./amount-step')
const infoStep = require('./info-step')
const followupStep = require('./followup-step')
const request = require('../../common/request')
const format = require('../../common/format')
const brandedWizard = require('../../components/styles/branded-wizard')
const renderStyles = require('../../components/styles/render-styles')
renderStyles()(brandedWizard(null))
// pass in a stream of configuration parameters
const init = params$ => {
var state = {
error$: flyd.stream()
, loading$: flyd.stream()
, clickLogout$: flyd.stream()
, clickFinish$: flyd.stream()
, params$: flyd.map(getParams, params$)
}
app.iframeParams = app.iframeParams || ""
app.utmParams = app.utmParams || {}
// maps utmParams from URL params string into object:
// { $utm_param: … } if params from iframe are present
app.iframeParams = app.iframeParams.split("?")[2] ? Object.assign(...app.iframeParams.split("?")[2].split("&").map((param) => param.split("=")).map(
array => ({[array[0]]: array[1]}))
) : {}
app.utmParams = {
utm_campaign: app.utmParams.utm_campaign || app.iframeParams.utm_campaign,
utm_content: app.utmParams.utm_content || app.iframeParams.utm_content,
utm_medium: app.utmParams.utm_medium || app.iframeParams.utm_medium,
utm_source: app.utmParams.utm_source || app.iframeParams.utm_source
}
app.campaign = app.campaign || {} // so we don't have to hot switch all the calls to app.campaign.name, etc
var donationDefaults = setDonationFromParams({
nonprofit_id: app.nonprofit_id
, campaign_id: app.campaign.id
, event_id: app.event_id
}, state.params$())
state.selectedPayment$ = flyd.stream('sepa')
state.amountStep = amountStep.init(donationDefaults, state.params$)
state.infoStep = infoStep.init(state.amountStep.donation$, state)
state.donation$ = scanMerge([
[state.amountStep.donation$, R.merge]
, [state.infoStep.savedSupp$, (d, supp) => R.assoc('supporter_id', supp.id, d)]
, [state.params$, setDonationFromParams]
, [state.infoStep.savedDedicatee$, setDonationDedication]
], donationDefaults )
state.paymentStep = paymentStep.init({
supporter$: state.infoStep.savedSupp$
, donation$: state.donation$
, dedicationData$: state.infoStep.dedicationData$
, activePaymentTab$: state.selectedPayment$
, params$: state.params$
})
const currentStep$ = flyd.mergeAll([
state.amountStep.currentStep$
, state.infoStep.currentStep$
, flyd.map(R.always(0), state.params$) // if the params ever change, jump back to step one
, flyd.stream(0)
])
state.wizard = wizard.init({currentStep$, isCompleted$: state.paymentStep.paid$})
// Save dedication as a supporter note once the donation is saved
// Requires the donor supporter, the dedicatee supporter, the dedication form data, and the paid donation
const dedicationParams$ = flyd.zip([state.infoStep.savedDedicatee$, state.infoStep.savedSupp$, state.paymentStep.paid$])
const savedDedication$ = flyd.flatMap(R.apply(postDedication), dedicationParams$)
// Log people out
flyd.map(ev => {request({method: 'get', path: '/users/sign_out'}); window.location.reload()}, state.clickLogout$)
// Handle the Finish button from the followup step -- will close modal, redirect, or refresh
flyd.lift(
(ev, params) => {
if(!parent) return
if(params.redirect) parent.postMessage(`commitchange:redirect:${params.redirect}`, '*')
else if(params.mode !== 'embedded'){
parent.postMessage('commitchange:close', '*');
} else {
if (window.parent) {window.parent.postMessage('commitchange:close', '*');};
}
}
, state.clickFinish$, state.params$ )
return state
}
const setDonationFromParams = (don, params) => {
if(!params.single_amount || isNaN(format.dollarsToCents(params.single_amount))) delete params.single_amount
return R.merge({
amount: params.single_amount ? format.dollarsToCents(params.single_amount) : 0
, recurring: params.type === 'recurring'
, gift_option_id: params.gift_option_id
, designation: params.designation
}, don)
}
// Set the text field to save to the server as serialized JSON
const setDonationDedication = (don, dedication) => {
return R.assoc(
'dedication'
, JSON.stringify({
supporter_id: dedication.supporter.id
, name: dedication.supporter.name
, contact: R.join(" - ", [dedication.supporter.email, dedication.supporter.phone, dedication.supporter.address])
, note: dedication.note
, type: dedication.type
})
, don)
}
// Save a dedication to the server by saving a note to the supporter
const postDedication = (dedication, donor, donation) => {
const pathPrefix = `/nonprofits/${ENV.nonprofitID}`
// TODO: translate content
var content = `[${donor.name}](${pathPrefix}/supporters?sid=${donor.id}) made a [donation of $${format.centsToDollars(donation.donation.amount)}](${pathPrefix}/payments?pid=${donation.payment.id}) in ${dedication.type || 'honor'} of this person.`
if(dedication.note) content += ` ${I18n.t('nonprofits.donate.dedication.donor_note')} "${dedication.note}".`
return flyd.map(r => r.body, request({
method: 'post'
, path: `/nonprofits/${app.nonprofit_id}/supporters/${dedication.supporter.id}/supporter_notes`
, send: {supporter_note: {supporter_id: dedication.supporter.id, user_id: ENV.support_user_id, content}}
}).load)
}
const view = state => {
return h('div.js-donateForm', {
class: {'is-modal': state.params$().offsite}
}, [
h('img.closeButton', {
props: {src: '/assets/ui_components/close.svg'}
, on: {click: ev => state.params$().offsite ? parent.postMessage('commitchange:close', '*') : null}
, class: {'u-hide': !state.params$().offsite}
})
, h('div.titleRow', [
h('img', {props: {src: app.nonprofit.logo.normal.url}})
, h('div.titleRow-info', [
h('h2', app.campaign.name || app.nonprofit.name )
, h('p', [
state.params$().designation && !state.params$().single_amount
? headerDesignation(state)
: app.campaign.tagline || app.nonprofit.tagline || ''
])
])
])
, wizardWrapper(state)
, h('footer.donateForm-footer', {
class: {'u-hide': !app.user}
}, [
h('span', `${I18n.t('nonprofits.donate.signed_in')} `)
, h('strong', String(app.user && app.user.email))
, h('a.logout-button', {on: {click: state.clickLogout$}}, ` ${I18n.t('nonprofits.donate.log_out')}`)
])
])
}
const headerDesignation = state => {
return h('span', [
h('i.fa.fa-star', {style: {color: app.nonprofit.brand_color || ''}})
, h('strong', ` ${I18n.t('nonprofits.donate.amount.designation.label')} `)
, String(state.params$().designation)
, state.params$().designation_desc
? h('span', [h('br'), h('small', state.params$().designation_desc)])
: ''
])
}
const wizardWrapper = state => {
return h('div.wizard-steps.donation-steps', [
wizard.view(R.merge(state.wizard, {
steps: [
{name: I18n.t('nonprofits.donate.amount.label'), body: amountStep.view(state.amountStep)}
, {name: I18n.t('nonprofits.donate.info.label'), body: infoStep.view(state.infoStep)}
, {name: I18n.t('nonprofits.donate.payment.label'), body: paymentStep.view(state.paymentStep)}
]
, followup: followupStep.view(state)
}))
])
}
module.exports = {view, init}