The configuration options are set in a json attribute on the actionPage, and then stored into config/{actionpageid}.json
For the rest of the document, we are using 42 as the action page id, obviously replace with the real id of your widget.
you might need to switch between multiple servers (eg staging and production). Each of the yarn commands can be prefaced by PROCA_ENV=[staging|production|whatever] to specify the environment to us each of the ENV (eg staging) is set into a .env.staging and should contain
REACT_APP_API_URL='https://staging.url/api' # default https://api.proca.app/api
REACT_APP_CONFIG_FOLDER='config.staging' # default config
AUTH_USER=your_auth
AUTH_PASSWORD=your_secret
pulls the configuration as stored on the proca server
yarn pull 42
edit the configuration (file config/42.json)
to check the configuration, run a local copy of the widget
yarn start 42
once you are happy with the setting, save it back to the server
yarn push 42
and build the widget
yarn build 42
You can edit them, run
for the "simpler" changes, use the admin interface at dash.proca.app (for the hosted version)
the main way of changing the configuration is changing the json configuration into the config folder and building the widget
check the documentation on the widget too
if present on the page, the function procaReady () is called during the initialization of the widget.
Putting calls to change the layout there is best because it prevents the widget to be displayed with the old layout first and then with the one you want.
function procaReady () {
proca.set("layout", "primaryColor",'#cafebebe')
proca.set("layout","variant", 'filled') // one of ["standard", "filled", "outlined"]
proca.set("layout","theme","light") // one of ["dark","light"]);
}
some things like the button text or the share messages can be set in the html
you need to put two classes "proca-text" (to tell proca it contains a text you want to use). tip: all these will be hidden after the widget loads
and the name of the element you want to override
sign-now,register,share_title,share_intro,consent_intro,consent_opt-in,consent_opt-in,email_subject,email_body,twitter_actionText, share,share-twitter,share-whatsapp,share-subject,share-body,dialog-title
so for instance, if you want to change the share message on whatsapp:
<span class='proca-text share-whatsapp'>Hey, I signed this petition and I thought you would be interested to sign it too!</span>
Each step of the widget. they are matching a react component into src/components, for instance
- Petition -> src/components/Petition.js
- Share -> src/components/Share.js
you can (from outside of the widget), jump to the next step with
proca.go()
or to any step with
proca.go("Petition")
it can also be set from the url as a get param ?proca_go=Petition
To redirect to some URL after submitting:
- "Redirect" in journey
config.component.redirect.url: "whateverURL"
if true (or set from the url as a get param ?proca_test
- display a message snackbar "test"
- store the actions as "test" instead of the normal "petition" or "register" or "donate"
- use the test config when relevant (eg for the payment providers)
primaryColor: #rgb
. It will set the color for the buttons, progress bar and wherever material-ui is using the primary color ;)secondaryColor
. Not sure how it works/where it is used, never changed the defaulttheme: "standard", "filled", "outlined"
. Check the material-ui doc for examplestemplate: "eci.html"
. It's for the "demo" page, it takes as a template the file public/{HtmlTemplate}.html
This is the big chunk of the configuration options for each component.
most components are using the hook useCampaignConfig to read that config and adjust their behaviour accordingly.
Tip: When developing, always have a sensible default behaviour even if the specific config.component isn't set. if you add a new feature, by default, the component should work as before (backward compatibility) until you enable the new feature into the config file
the main behaviour
config.component.widget.fab: boolean
. If true, a floating action button is displayed when form is not. It is available for all screen sizes. It is true by default.config.component.widget.mobileVersion: boolean
. If true, a floating action button is displayed when form is not. It is available for all screen sizes. (It has nothing to do with mobile version anymore). Deprecated: Activates a mobile layout version - if true, a floating action button is displayed instead of the normal widget on mobile screens only, and the widget steps are displayed (after the click) on full screen.config.component.widget?.autoStart: boolean
. default true. used to not automatically display the widget. see portal Clickify belowconfig.component.widget.forceWidth: number
. by default, the width of the widget is the width available on the page (reactive). Can be used to force a width, for instance to embed into broken or weird html pages ;)config.component.widget.delay": number
. delay creating the widget by number milliseconds, mostly to let crappy javascript into the page do their mess before the widget is displayed
Register is the main form used to collect personal data and consent.
- validateEmail: (default:true) do not use the micro-service that validates that the domain is valid and able to receive emails. Will increase number of invalid emails if false
- tracking: (default: true) do not register the utm parameters or the (anonymised) location. Might decrease security if false
- remember (boolean), if true, save the userid and firstname into a cookie, so next time they take an action, they do not have to fill the form
most fields can either be displayed or hidden, most fields can either be required or not. First name and emails are always displayed AND required
config.component.register.field.organisation
. do we collect the organisation details (with prefill options using twitter)config.component.register.field.lastname: {required:boolean}
config.component.register.field.postcode: boolean (show/hide) or {required:boolean}
config.component.register.field.country: boolean (show/hide) or {required:boolean}
config.component.register.field.comment: boolean (show/hide) or {required:boolean}
config.component.register.field.phone: boolean
. (show/hide)
To add helper text for the comment (for example: "Your message may be used..."):
locales.common.help.comment: "Your message may be used..."
config.component.wall.comments.filter: FILTER_NAME
FILTER_NAME is column in Supabase and it is filtered if FILTER_NAME === true
To show:
config.component.counter: true
To hide if support count lower than some number:
config.component.counter.min: number
To customize steps:
The counter will automatically calculate next goals so that goal always escapes the current number (keeps being motivating but is not reached). You can set the goal "steps" - successive signature counts to reach by setting an array config.component.counter.steps
. Set to single value ([1000]
) to fix the goal at 1000.
Counter is always shown in test mode!
A step with customizable text and button.
For text:
"component": { "markdown": { "text": "campaign:thankspage.text", "title": "campaign:thankspage.title", "next": false } }
Next button is shown unless config.component.markdown.next: false
config.component.country
. 2 char iso code (default country), or boolean (disable/enable ip geolooup)config.component.country.other
(boolean) add an "other" option on the drop down (it always add the country of the ip address of the visitor)
Set country to XX
, register.field.country: false
and register.field.locality: true
. Technically, you can have both the country and locality, but works better if the country is outside of the form, for instance for mail2target a actions
Country covered so far (and postcode length):
AT: 4, BE: 4, CH: 4, DE: 5, FR: 5, IT: 5, PL: 5, CA: 5, DK: 4, NL: 4
to collect the consent of being contacted
config.component.consent.implicit: boolean or "opt-in"
. default false. Besides when using it as a registration form, very few GDPR valid reasons to have implicit consent. Don't use it until you really know what you're doingconfig.component.consent.privacyPolicy
. very important: the privacy policy of the organization embedding the widget. It's taken automatically from the org.config privacy policy if setconfig.component.consent.split
. if the widget is for a partner, is the consent split (would you like to be contacted by 1) opt-in partner 2) opt-in partner+lead 3) opt-outconfirm.component.consent.confirmProcessing
. Add radio buttons (required) with the text of the consent processing.
config.component.consent.implicit: true
and confirm.component.consent.confirmProcessing: true
- adds a checkbox.
config.component.implicit: "opt-in" - adds a line 'Submit this form to receive emails from {{org}}'
config.component.consent.confirm === false- removes "Are you sure" question.
config.component.consent.benefit === false` - removes "Are you sure" when picture not added.
-
config.component.consent.email.confirmOptIn: boolean
If mail confirmation after form opt-in needed, it adds snackbar with "check email" message. -
config.component.consent.email.confirmAction: boolean
If mail needs to be verified before action is accepted, it adds snackbar with "check email" message. -
config.component.consent.email.sample: number
It limits the number of targets displayed. Filter (config.component.consent.email.filter: array
,) needs to have at least one string element,["random"]
usually.
- actionUrl: false if we add the url of the action page (at the end of where the {url} token is set
config.component.share.utm: boolean
. Are we adding the utm tracking codes to the shared url? (true by default)- config.component?.share?.compact: have a shorter form utm=proca.share.medium (not compatible with GTM, but shorter)
config.component.share.url: string
. Share this url instead of the page embedding the widgetconfig.component.share.anonymous: boolean
. Are we linking the share actions to the supporter or not? are we adding the utm tracking codes to the shared url?config.component.share.top: boolean
. Are the share button above or below image+text shared (taken from meta data)config.component.share.email: boolean
. Enable the share by email. Brocken now (on some config)config.component.share.reddit: boolean
: Enable share on reddit
config.component.register.button: 'whateverLabel'
component.eci.starts: step ("email")
"portal": ["layout/ClosedNotice"]
component.register.disabled: true
component.widget.closed: true
The Donate steps are configured under the config.component.donation
key.
- server: boolean sending from the server or client
- bcc: if sent from client, add a bcc
- bccOptout: if false, do not bcc if optout
config.component.email.field.message.disabled: boolean
. Add (non)editable message field.
<Select name="gender" locale="campaign:profile.gender" />
in campaign config
profile.gender: {"F":"Female","M":"Male","":"Other/prefer not say"}
or "gender": "Female\nMale\nOther"
or "gender": "F|Female\nM|Male"
config.component.email.replace = { "https://example.org/campaign":"https://example.org/campaign?proca_uuid={{target.externalId}}" }
Configure the currency.
"currency": {
"code": "EUR",
"symbol": "€"
}
The donation form can present multiple options for recurring or one-off donations. The 'frequency' section configures what options are shown. For each frequency, there are other configuration keys used to control how they are displayed. For example, if "monthly" is in frequency.options, amount.monthly should be set to a list of amounts to display when "monthly" is selected.
options
lists which frequencies to present as buttons on the form. The options monthly
, oneoff
are tested, weekly
and annually
are partly implemented but need translations and testing.
"frequency": {
"options": [ 'monthly' ] // all donations are monthly recurring
"options": [ 'monthly', 'oneoff' ] // show a choice between monthly and oneoff
}
Use default
to set which frequency to preselect.
If average is defined for a frequency, the form will display a message like "The average donation is X". You can change the message in the translation files.
"average": {
"monthly": 4.50,
"oneoff": 10.00
}
Sets the amounts to display for each frequency. If default
is set, it will be preselected.
"amount": {
"oneoff": [ 3, 5, 10, 25, 50, 100, 200 ],
"monthly": [ 5, 7, 8, 9, 10, 11 ],
"default": 5
}
if not set, paypal isn't offered a payment option
Configures the PayPal button. See the PayPal documentation for how to get the clientId
.
The planId
is used to create subscriptions. See the documentation for instructions on creating a plan.
Create a subscription plan with:
- Plan type: Quantity pricing
- Price per item: 0,01 EUR
- Billing cycle: Every 1 month
- Number of billing cycles: Unlimited
Example:
"paypal": {
"clientId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"planId": "P-XYZ"
}
Configures Stripe for card donations. See the Stripe documentation for getting the publishable API key. Subscriptions rely on a Product - see the documentation for creating a product. if you want, set up the testKey too, which will be used if the widget is in test mode (?proca_test is true)
Create the Product with a 1 € / month price.
Example:
"stripe": {
"productId": "prod_xyz",
"publishableKey": "pk_xyx"
"testKey": "pk_test_xyx"
}
The SEPA handler does nothing. The SEPA details are sent to the AMQP server - that's all. The sepa
key lets you enable the SEPA payment method.
Example:
"sepa": {
"enabled": true
}
If true the donation form will display a "title" above the form. The default is "I'm donating {{amount}} {{frequency}}" and can be updated using translations. The average
phrase will only be displayed if useTitle is true.
"useTitle": true
Set a title to display when no amount is selected.
This allows the widget to interact with other elements on the page (beside the "main" widget)
display the number of signatures on the page into the dom class="proca-counter"
{
"component": "Counter",
"selector": ".proca-counter"
}
display the title of the campaign into the dom class="eci-title"
{
"component": "T",
"message": "campaign:name",
"selector": ".eci-title"
},
start the widget journey when clicking on a link "#proca-dialog" (or with class .proca-button)
"Clickify"
this is used to have the widget activated on a button (the widget first step is a dialog and doesn't start automatically)
allows overwriting any of the standard text. It can also be used with the "T" component in portals above to automatically display the text of the petition (used to put the official text of the ECI for instance)
you can probably safely ignore that
components/Widget.js:58: if (config.component.widget?.mobileVersion === false) isMobile = false; components/Widget.js:131: if (config.component.widget?.autoStart === false) return setCurrent(null); components/Widget.js:220: if (config.component.widget?.autoStart !== false) { components/Widget.js:237: width={isMobile || config.component.widget?.forceWidth ? 0 : null} components/Iframe.js:16: let url = config.component.iframe.url; components/Iframe.js:24: if (config.component.iframe.hash) components/Iframe.js:25: url = url + "#" + config.component.iframe.hash; components/Iframe.js:39: config.component.iframe.successMessage && components/Iframe.js:40: event.data === config.component.iframe.successMessage components/Iframe.js:46: }, [done, config.component.iframe.successMessage, iframeOrigin]); components/Iframe.js:50: width={config.component.iframe.width} components/Iframe.js:51: height={config.component.iframe.height} components/Register.js:93: if (config.component.consent?.implicit) data.privacy = "opt-in"; components/Register.js:98: : config.component?.register?.actionType || "register", components/Register.js:124: if (!config.component.share?.anonymous) { components/Register.js:192: const ConsentBlock = config.component.consent?.implicit components/Register.js:209: {config.component?.register?.field.organisation && ( components/Register.js:242: {config.component?.register?.field?.postcode !== false && ( components/Register.js:250: config.component?.register?.field?.postcode?.required components/Register.js:255: {config.component?.register?.field?.country !== false && ( components/Register.js:260: {config.component?.register?.field?.phone === true && ( components/Register.js:265: {config.component?.register?.field?.comment !== false && ( components/Register.js:300: t(config.component.register?.button || "register")} components/Register.js:302: {config.component.register?.next && ( components/Ep.js:70: const committee = config.component.Ep.filter.committee; components/Ep.js:88: if (config.component?.twitter?.listUrl) components/Ep.js:89: fetchData(config.component.twitter.listUrl); components/Ep.js:91: }, [config.component, config, setAllProfiles]); components/Consent.js:65: config.component?.consent?.privacyPolicy || components/Consent.js:68: config.component?.country === false components/Consent.js:72: config.component?.consent?.intro === false components/Consent.js:79: const confirmOptOut = !(config.component.consent?.confirm === false); // by default we ask for confirmation components/Consent.js:102: {!config.component?.consent?.split && ( components/Consent.js:112: {config.component?.consent?.split && ( components/Email.js:52: if (!config.component.email?.filter?.includes("country")) components/Email.js:59: if (config.component?.email?.listUrl) { components/Email.js:60: fetchData(config.component.email.listUrl); components/Email.js:63: typeof config.component.email.to === "string" components/Email.js:64: ? config.component.email.to?.split(",") components/Email.js:74: }, [config.component, config.hook, setAllProfiles]); components/Email.js:112: const bcc = config.component.email?.bcc; components/Email.js:130: if (config.component.email?.cc === true) { components/Email.js:135: config.component.email?.to && components/Email.js:136: typeof config.component.email.to === "string" components/Email.js:138: to = config.component.email.to; components/Email.js:181: {config.component.email.progress && ( components/Email.js:184: {config.component.email?.filter?.includes("country") && ( components/Email.js:185: components/Email.js:187: {config.component.email?.showTo !== false && ( components/Country.js:86: country: config.data.country || config.component.country, components/EmailAction.js:74: onClick={config.component?.email?.split === true ? mail : null} components/EmailAction.js:81: {config.component?.email?.split === true && ( components/Clickify.js:3: // config.component.widget?.mobileVersion; components/campax/Initiative.js:71: config.component.initiative.prefixActionPage + components/italy/Register.js:106: config.test ? "test" : config.component?.register?.actionType || "sign", components/italy/Register.js:266: sitekey={config.component.register.hcaptcha} components/italy/Register.js:295: t(config.component.register?.button || "register")} components/italy/Consent.js:85: url: config.component.consent.privacyPolicy, components/italy/Consent.js:86: urlRegister: config.component.consent.content, components/eci/Details.js:13: const eci = config.component.eci; components/eci/Support.js:110: +config.component.eci.actionpage, components/eci/Support.js:112: { captcha: token, apiUrl: config.component.eci.apiUrl } components/eci/Support.js:249: geocountries={config.component.eci.geocountries} components/eci/Support.js:265: sitekey={config.component.eci.hcaptcha} components/eci/Stepper.js:22: config.component.eci.starts === "email" components/eci/Country.js:61: country: config.data.country || config.component.country, components/ImplicitConsent.js:45: config.component?.consent?.privacyPolicy || components/ImplicitConsent.js:48: config.component?.country === false components/Twitter.js:49: if (!config.component.twitter?.filter.includes("country")) components/Twitter.js:56: if (config.component?.twitter?.listUrl) components/Twitter.js:57: fetchData(config.component.twitter.listUrl); components/Twitter.js:58: }, [config.component, config.hook, setAllProfiles]); components/Twitter.js:107: {config.component.twitter?.filter?.includes("country") && ( components/Twitter.js:108: components/Share.js:119: {config.component.share?.top && <Actions {...props} />} components/Share.js:130: {!config.component.share?.top && <Actions {...props} />} components/Share.js:132: {config.component.share?.next && ( components/Share.js:173: {!!config.component?.share?.email && ( components/Share.js:182: {!!config.component?.share?.reddit && ( components/Share.js:196: if (config.component.share?.anonymous === true) return; // do not record the share if anonymous components/donate/Sepa.js:53: config.test ? "test" : config.component?.register?.actionType || "donate", components/donate/Sepa.js:136: {config.component?.donate?.field?.phone === true && ( components/ProgressCounter.js:76: const goal = nextStep(count, config.component.counter?.steps); components/ProgressCounter.js:77: const separator = config.component.counter?.separator; components/Mep.js:46: if (config.component?.twitter?.listUrl) components/Mep.js:47: fetchData(config.component.twitter.listUrl); components/Mep.js:48: }, [config.component, config.hook, setAllProfiles]); hooks/usePaypal.js:102: if (!config.component.donation?.paypal?.clientId) return; hooks/usePaypal.js:105: (config.test ? "sb" : config.component.donation.paypal.clientId || "sb"); hooks/useCount.js:38: const apiUrl = config.component?.useCount?.apiUrl || null; components/donate/Amount.js:122: const average = config.component.donation?.amount?.oneoff?.average; components/donate/Amount.js:128: : config.component.donation?.subTitle; components/donate/Amount.js:129: const image = config.component.donation?.image; components/donate/Amount.js:144: if (config.component.donation.external?.url) { components/donate/Amount.js:163: config.component.donation.external.url + amount + params, components/donate/Amount.js:213: {config.component.donation?.monthly !== false && ( components/donate/Amount.js:245: {!config.component.donation.external && (