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