houdini/client/js/nonprofits/donate/wizard.js
Bradley M. Kuhn fc77ee76d6 Relicense Javascript code in accordance with project's new license
The primary license of the project is changing to:
  AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later

The Additional Permission is designed to permit publicly distributed
Javascript code to be relicensed under LGPL-3.0-or-later, but not server-side
Javascript code.  As such, we've relicensed here static Javscript files under
LGPL-3.0-or-later, and those that run as part of build and/or server side
under AGPL-3.0-or-later.

Note that in future, Javascript files may be updated to be stronger copyleft
license with the Additional Permission, particularly if they adapted to run
on server side and/or turned into templates.  Of course, we'd seek public
discussion with the contributor community about such changes.

This commit is one of the many steps to relicense the entire codebase.

Documentation granting permission for this relicensing (from all past
contributors who hold copyrights) is on file with Software Freedom
Conservancy, Inc.
2018-03-25 15:10:40 -04:00

206 lines
7.7 KiB
JavaScript

// 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}