basic support for Stripe Custom accounts
This commit is contained in:
parent
9aa647a6f0
commit
209600b217
@ -16,6 +16,7 @@ Liberapay.init = function() {
|
||||
Liberapay.lookup.init();
|
||||
Liberapay.s3_uploader_init();
|
||||
Liberapay.stripe_init();
|
||||
Liberapay.stripe_connect.init();
|
||||
|
||||
$('div[href]').css('cursor', 'pointer').on('click auxclick', function(event) {
|
||||
if (event.target.tagName == 'A') {
|
||||
|
97
js/stripe-connect.js
Normal file
97
js/stripe-connect.js
Normal file
@ -0,0 +1,97 @@
|
||||
Liberapay.stripe_connect = {};
|
||||
|
||||
Liberapay.stripe_connect.init = function() {
|
||||
var container = document.getElementById('stripe-connect');
|
||||
if (!container) return;
|
||||
|
||||
async function fetchClientSecret() {
|
||||
try {
|
||||
var response = await fetch('', {
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"X-CSRF-TOKEN": container.dataset.csrfToken,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
if (response.ok) {
|
||||
return (await response.json()).client_secret;
|
||||
} else {
|
||||
Liberapay.error(response.status);
|
||||
}
|
||||
} catch(exc) {
|
||||
Liberapay.error(exc);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const self = Liberapay.stripe_connect;
|
||||
self.components = {};
|
||||
const component_nav = document.getElementById('stripe-component-nav');
|
||||
var target_component_name;
|
||||
if (component_nav) {
|
||||
target_component_name = 'account-management';
|
||||
component_nav.querySelector(
|
||||
'a[data-component="' + target_component_name + '"]'
|
||||
).parentElement.classList.add('active');
|
||||
component_nav.classList.remove('hidden');
|
||||
const component_nav_links = component_nav.querySelectorAll('a');
|
||||
component_nav_links.forEach((a) => {
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
component_nav_links.forEach((a) => {
|
||||
a.parentElement.classList.remove('active')
|
||||
});
|
||||
a.parentElement.classList.add('active');
|
||||
const component_name = a.dataset.component;
|
||||
if (self.components[component_name]) {
|
||||
self.components[component_name].classList.remove('hidden');
|
||||
} else {
|
||||
self.components[component_name] = self.instance.create(
|
||||
component_name
|
||||
);
|
||||
container.appendChild(self.components[component_name]);
|
||||
}
|
||||
self.current_component.classList.add('hidden');
|
||||
self.current_component = self.components[component_name];
|
||||
container.scrollIntoView();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
target_component_name = 'account-onboarding';
|
||||
}
|
||||
|
||||
if (!window.StripeConnect) {
|
||||
alert(container.dataset.msgStripeMissing);
|
||||
return;
|
||||
}
|
||||
StripeConnect.onLoad = () => {
|
||||
self.instance = StripeConnect.init({
|
||||
appearance: {
|
||||
variables: {
|
||||
colorPrimary: '#337ab7',
|
||||
colorText: rgb_to_hex($(container).css('color')),
|
||||
fontFamily: $(container).css('font-family'),
|
||||
fontSizeBase: $(container).css('font-size'),
|
||||
},
|
||||
},
|
||||
fetchClientSecret: fetchClientSecret,
|
||||
fonts: [{cssSrc: 'https://liberapay.com/assets/fonts.css'}],
|
||||
locale: document.documentElement.getAttribute('lang'),
|
||||
publishableKey: container.dataset.stripePubKey,
|
||||
});
|
||||
const notification_div = document.getElementById('stripe-notification');
|
||||
if (notification_div) {
|
||||
notification_div.appendChild(self.instance.create('notification-banner'));
|
||||
}
|
||||
var component = self.instance.create(target_component_name);
|
||||
self.current_component = self.components[target_component_name] = component;
|
||||
if (target_component_name == 'account-onboarding') {
|
||||
component.setOnExit(() => {
|
||||
window.location = location.href.replace(
|
||||
/\/stripe\/onboard\/?\?.+$/, '/'
|
||||
);
|
||||
});
|
||||
}
|
||||
container.appendChild(self.current_component);
|
||||
}
|
||||
};
|
54
js/stripe.js
54
js/stripe.js
@ -1,11 +1,6 @@
|
||||
Liberapay.stripe_init = function() {
|
||||
var $form = $('form#stripe');
|
||||
if ($form.length === 1) Liberapay.stripe_form_init($form);
|
||||
var $next_action = $('#stripe_next_action');
|
||||
if ($next_action.length === 1) Liberapay.stripe_next_action($next_action);
|
||||
};
|
||||
|
||||
Liberapay.stripe_form_init = function($form) {
|
||||
if ($form.length !== 1) return;
|
||||
$('fieldset.hidden').prop('disabled', true);
|
||||
$('button[data-modify]').click(function() {
|
||||
var $btn = $(this);
|
||||
@ -13,6 +8,15 @@ Liberapay.stripe_form_init = function($form) {
|
||||
$btn.parent().addClass('hidden');
|
||||
});
|
||||
|
||||
var $errorElement = $('#stripe-errors');
|
||||
var stripe = null;
|
||||
if (window.Stripe) {
|
||||
stripe = Stripe($form.data('stripe-pk'));
|
||||
} else {
|
||||
$errorElement.text($form.attr('data-msg-stripe-missing'));
|
||||
$errorElement.hide().fadeIn()[0].scrollIntoView();
|
||||
}
|
||||
|
||||
var $container = $('#stripe-element');
|
||||
var $postal_address_alert = $form.find('.msg-postal-address-required');
|
||||
var $postal_address_country = $form.find('select[name="postal_address.country"]');
|
||||
@ -32,10 +36,7 @@ Liberapay.stripe_form_init = function($form) {
|
||||
);
|
||||
}
|
||||
|
||||
var $errorElement = $('#stripe-errors');
|
||||
var stripe = null;
|
||||
if (window.Stripe) {
|
||||
stripe = Stripe($form.data('stripe-pk'));
|
||||
if ($container.length === 1) {
|
||||
var elements = stripe.elements({
|
||||
onBehalfOf: $form.data('stripe-on-behalf-of') || undefined,
|
||||
});
|
||||
@ -66,14 +67,29 @@ Liberapay.stripe_form_init = function($form) {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$errorElement.text($form.attr('data-msg-stripe-missing'));
|
||||
}
|
||||
|
||||
Liberapay.stripe_before_submit = async function() {
|
||||
Liberapay.stripe_before_account_submit = async function() {
|
||||
const response = await stripe.createToken('account', {
|
||||
tos_shown_and_accepted: true,
|
||||
});
|
||||
if (response.token) {
|
||||
$form.find('input[name="account_token"]').remove();
|
||||
var $pm_id_input = $('<input type="hidden" name="account_token">');
|
||||
$pm_id_input.val(response.token.id);
|
||||
$pm_id_input.appendTo($form);
|
||||
return true;
|
||||
} else {
|
||||
$errorElement.text(response.error || response);
|
||||
$errorElement.hide().fadeIn()[0].scrollIntoView();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Liberapay.stripe_before_element_submit = async function() {
|
||||
// If the Payment Element is hidden, simply let the browser submit the form
|
||||
if ($container.parents('.hidden').length > 0) {
|
||||
console.debug("stripe_before_submit: ignoring hidden payment element");
|
||||
console.debug("stripe_before_element_submit: ignoring hidden payment element");
|
||||
return true;
|
||||
}
|
||||
// If Stripe.js is missing, stop the submission
|
||||
@ -125,14 +141,4 @@ Liberapay.stripe_form_init = function($form) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Liberapay.stripe_next_action = function ($next_action) {
|
||||
stripe.handleCardAction($next_action.data('client_secret')).then(function (result) {
|
||||
if (result.error) {
|
||||
$next_action.addClass('alert alert-danger').text(result.error.message);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ from liberapay.payin.cron import (
|
||||
send_upcoming_debit_notifications,
|
||||
)
|
||||
from liberapay.security import authentication, csrf, set_default_security_headers
|
||||
from liberapay.security.csp import csp_allow, csp_allow_stripe
|
||||
from liberapay.security.csp import csp_allow, csp_allow_stripe, csp_allow_stripe_connect
|
||||
from liberapay.utils import (
|
||||
b64decode_s, b64encode_s, erase_cookie, http_caching, set_cookie,
|
||||
)
|
||||
@ -412,6 +412,10 @@ if hasattr(pando.Response, 'csp_allow_stripe'):
|
||||
raise Warning('pando.Response.csp_allow_stripe() already exists')
|
||||
pando.Response.csp_allow_stripe = csp_allow_stripe
|
||||
|
||||
if hasattr(pando.Response, 'csp_allow_stripe_connect'):
|
||||
raise Warning('pando.Response.csp_allow_stripe_connect() already exists')
|
||||
pando.Response.csp_allow_stripe_connect = csp_allow_stripe_connect
|
||||
|
||||
if hasattr(pando.Response, 'encode_url'):
|
||||
raise Warning('pando.Response.encode_url() already exists')
|
||||
def _encode_url(url):
|
||||
|
@ -462,6 +462,7 @@ def resolve_team_donation(
|
||||
AND a.country IN %(SEPA)s
|
||||
ORDER BY a.participant
|
||||
, a.default_currency = %(currency)s DESC
|
||||
, a.country = %(payer_country)s DESC
|
||||
, a.connection_ts
|
||||
""", dict(locals(), SEPA=SEPA, member_ids={t.member for t in takes}))}
|
||||
if sepa_only or len(sepa_accounts) > 1 and takes[0].member in sepa_accounts:
|
||||
|
@ -43,3 +43,14 @@ def csp_allow_stripe(response) -> None:
|
||||
(b'frame-src', b"*.js.stripe.com js.stripe.com hooks.stripe.com"),
|
||||
(b'script-src', b"*.js.stripe.com js.stripe.com"),
|
||||
)
|
||||
|
||||
|
||||
def csp_allow_stripe_connect(response) -> None:
|
||||
# https://docs.stripe.com/security/guide?csp=csp-connect#content-security-policy
|
||||
csp_allow(
|
||||
response,
|
||||
(b'frame-src', b"connect-js.stripe.com js.stripe.com"),
|
||||
(b'img-src', b"*.stripe.com"),
|
||||
(b'script-src', b"connect-js.stripe.com js.stripe.com"),
|
||||
(b'style-src', b"sha256-0hAheEzaMe6uXIKV4EehS9pu1am1lj/KnnzrOYqckXk="),
|
||||
)
|
||||
|
@ -398,15 +398,17 @@ class Harness(unittest.TestCase):
|
||||
data.setdefault('verified', True)
|
||||
data.setdefault('display_name', None)
|
||||
data.setdefault('token', None)
|
||||
data.setdefault('independent', True)
|
||||
data.setdefault('loss_taker', 'provider')
|
||||
data.update(p_id=participant.id, provider=provider, country=country)
|
||||
r = self.db.one("""
|
||||
INSERT INTO payment_accounts
|
||||
(participant, provider, country, id,
|
||||
default_currency, charges_enabled, verified,
|
||||
display_name, token)
|
||||
display_name, token, independent, loss_taker)
|
||||
VALUES (%(p_id)s, %(provider)s, %(country)s, %(id)s,
|
||||
%(default_currency)s, %(charges_enabled)s, %(verified)s,
|
||||
%(display_name)s, %(token)s)
|
||||
%(display_name)s, %(token)s, %(independent)s, %(loss_taker)s)
|
||||
RETURNING *
|
||||
""", data)
|
||||
participant.set_attributes(payment_providers=self.db.one("""
|
||||
|
14
sql/branch.sql
Normal file
14
sql/branch.sql
Normal file
@ -0,0 +1,14 @@
|
||||
BEGIN;
|
||||
CREATE TYPE loss_taker AS ENUM ('provider', 'platform');
|
||||
ALTER TABLE payment_accounts
|
||||
ADD COLUMN independent boolean DEFAULT true,
|
||||
ADD COLUMN loss_taker loss_taker DEFAULT 'provider',
|
||||
ADD COLUMN details_submitted boolean,
|
||||
ADD COLUMN allow_payout boolean;
|
||||
END;
|
||||
SELECT 'after deployment';
|
||||
BEGIN;
|
||||
ALTER TABLE payment_accounts
|
||||
ALTER COLUMN independent DROP DEFAULT,
|
||||
ALTER COLUMN loss_taker DROP DEFAULT;
|
||||
END;
|
@ -165,8 +165,6 @@ elif payin_id:
|
||||
except NextAction as act:
|
||||
if act.type == 'redirect_to_url':
|
||||
response.refresh(state, url=act.redirect_to_url.url)
|
||||
elif act.type == 'use_stripe_sdk':
|
||||
client_secret = act.client_secret
|
||||
else:
|
||||
raise NotImplementedError(act.type)
|
||||
tell_payer_to_fill_profile = (
|
||||
@ -256,13 +254,7 @@ title = _("Funding your donations")
|
||||
|
||||
% block thin_content
|
||||
|
||||
% if client_secret is defined
|
||||
|
||||
<p>{{ _("More information is required in order to process this payment.") }}</p>
|
||||
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
|
||||
<div id="stripe_next_action" data-client_secret="{{ client_secret }}"></div>
|
||||
|
||||
% elif payin is defined
|
||||
% if payin is defined
|
||||
% set status = payin.status
|
||||
% if status == 'succeeded'
|
||||
<div class="alert alert-success">{{ _(
|
||||
@ -354,7 +346,7 @@ title = _("Funding your donations")
|
||||
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
|
||||
|
||||
<form action="javascript:" method="POST" id="stripe"
|
||||
data-before-submit="call:Liberapay.stripe_before_submit"
|
||||
data-before-submit="call:Liberapay.stripe_before_element_submit"
|
||||
data-msg-stripe-missing='{{ _(
|
||||
"The initialization of a required component has failed. If you use a "
|
||||
"browser extension that blocks requests, for example NoScript, please "
|
||||
|
@ -18,7 +18,7 @@ if request.method == 'POST':
|
||||
AND pk = %s
|
||||
RETURNING *
|
||||
""", (participant.id, account_pk))
|
||||
if account and account.provider == 'stripe':
|
||||
if account and account.provider == 'stripe' and account.independent:
|
||||
try:
|
||||
stripe.oauth.OAuth.deauthorize(stripe_user_id=account.id)
|
||||
except stripe.oauth_error.InvalidClientError as e:
|
||||
@ -190,17 +190,41 @@ subhead = _("Payment Processors")
|
||||
% endif
|
||||
</p>
|
||||
<p class="text-muted">{{ _("Added on {date}", date=account.connection_ts.date()) }}</p>
|
||||
% if not account.charges_enabled
|
||||
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
|
||||
"This account cannot receive payments. To fix this, log in to the "
|
||||
"account and complete the verification process. After that, reconnect "
|
||||
"the account if you still see this message."
|
||||
) }}</p>
|
||||
% if account.independent
|
||||
% if not account.charges_enabled
|
||||
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
|
||||
"This account cannot receive payments. To fix this, log in to the "
|
||||
"account and complete the verification process. After that, reconnect "
|
||||
"the account if you still see this message."
|
||||
) }}</p>
|
||||
% endif
|
||||
<a href="https://dashboard.stripe.com/{{ account.id }}" target="_blank" rel="noopener noreferrer">{{
|
||||
icon("external-link") }} {{ _(
|
||||
"Manage this {platform} account", platform="Stripe"
|
||||
) }}</a>
|
||||
% elif account.details_submitted
|
||||
% if not account.charges_enabled
|
||||
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
|
||||
"This account cannot receive payments. To fix this, click "
|
||||
"on the link below and complete the verification process."
|
||||
) }}</p>
|
||||
% endif
|
||||
<a href="{{ participant.path('payment/stripe/manage?sn=%s' % account.pk) }}">{{
|
||||
icon("gear") }} {{ _(
|
||||
"Manage this {platform} account", platform="Stripe"
|
||||
) }}</a>
|
||||
% else
|
||||
% if not account.charges_enabled
|
||||
<p class="text-warning">{{ icon('exclamation-sign') }} {{ _(
|
||||
"This account cannot receive payments. To fix this, click "
|
||||
"on the link below and complete the activation process."
|
||||
) }}</p>
|
||||
% endif
|
||||
<a href="{{ participant.path('payment/stripe/onboard?sn=%s' % account.pk) }}">{{
|
||||
icon("manual") }} {{ _(
|
||||
"Activate this {platform} account", platform="Stripe"
|
||||
) }}</a>
|
||||
% endif
|
||||
<a href="https://dashboard.stripe.com/{{ account.id }}" target="_blank" rel="noopener noreferrer">{{
|
||||
icon("external-link") }} {{ _(
|
||||
"Manage this {platform} account", platform="Stripe"
|
||||
) }}</a>
|
||||
</form>
|
||||
% endfor
|
||||
</div>
|
||||
|
146
www/%username/payment/stripe/create.spt
Normal file
146
www/%username/payment/stripe/create.spt
Normal file
@ -0,0 +1,146 @@
|
||||
import stripe
|
||||
|
||||
from liberapay.utils import get_participant
|
||||
|
||||
[---]
|
||||
participant = get_participant(state, restrict=True, allow_member=False)
|
||||
|
||||
if request.method == 'POST':
|
||||
if user != participant:
|
||||
raise response.error(403)
|
||||
country = request.body.get_choice('country', constants.PAYOUT_COUNTRIES['stripe'])
|
||||
country_in_sepa = country in constants.SEPA
|
||||
account_token_id = request.body['account_token']
|
||||
if website.app_conf.stripe_secret_key.startswith('sk_test_'):
|
||||
profile_url = f"https://fake.liberapay.com/{participant.username}"
|
||||
else:
|
||||
profile_url = participant.url()
|
||||
account = stripe.Account.create(
|
||||
account_token=account_token_id,
|
||||
business_profile={
|
||||
"url": profile_url,
|
||||
},
|
||||
controller={
|
||||
"fees": {
|
||||
"payer": "application",
|
||||
},
|
||||
"losses": {
|
||||
"payments": "application",
|
||||
},
|
||||
"requirement_collection": "application",
|
||||
"stripe_dashboard": {
|
||||
"type": "none",
|
||||
},
|
||||
},
|
||||
capabilities={
|
||||
"transfers": {"requested": True},
|
||||
} if country_in_sepa else {
|
||||
"card_payments": {"requested": True},
|
||||
"transfers": {"requested": True},
|
||||
},
|
||||
country=country,
|
||||
metadata={
|
||||
"participant_id": str(participant.id),
|
||||
},
|
||||
settings={
|
||||
"payouts": {
|
||||
"debit_negative_balances": True,
|
||||
"schedule": {
|
||||
"interval": "manual",
|
||||
},
|
||||
},
|
||||
},
|
||||
idempotency_key=f"create_{country}_account_for_{participant.id}",
|
||||
)
|
||||
independent = (
|
||||
account.type == 'standard' or
|
||||
account.controller.stripe_dashboard.type != "none"
|
||||
)
|
||||
if account.type == 'standard' or account.controller.losses.payments == 'stripe':
|
||||
loss_taker = 'provider'
|
||||
else:
|
||||
loss_taker = 'platform'
|
||||
serial_number = website.db.one("""
|
||||
UPDATE payment_accounts
|
||||
SET is_current = NULL
|
||||
WHERE participant = %(p_id)s
|
||||
AND provider = 'stripe'
|
||||
AND country = %(country)s;
|
||||
|
||||
INSERT INTO payment_accounts
|
||||
(participant, provider, country, id,
|
||||
default_currency, charges_enabled, verified,
|
||||
display_name, independent, loss_taker)
|
||||
VALUES (%(p_id)s, 'stripe', %(country)s, %(account_id)s,
|
||||
%(default_currency)s, %(charges_enabled)s, true,
|
||||
%(display_name)s, %(independent)s, %(loss_taker)s)
|
||||
ON CONFLICT (provider, id, participant) DO UPDATE
|
||||
SET is_current = true
|
||||
, country = excluded.country
|
||||
, default_currency = excluded.default_currency
|
||||
, charges_enabled = excluded.charges_enabled
|
||||
, verified = true
|
||||
, display_name = excluded.display_name
|
||||
, independent = excluded.independent
|
||||
, loss_taker = excluded.loss_taker
|
||||
RETURNING pk;
|
||||
""", dict(
|
||||
p_id=participant.id,
|
||||
country=country,
|
||||
account_id=account.id,
|
||||
default_currency=account.default_currency.upper(),
|
||||
charges_enabled=account.charges_enabled,
|
||||
display_name=account.settings.dashboard.display_name,
|
||||
independent=independent,
|
||||
loss_taker=loss_taker,
|
||||
))
|
||||
raise response.redirect(participant.path(
|
||||
f'payment/stripe/onboard?sn={serial_number}'
|
||||
))
|
||||
|
||||
title = _("Create a {provider} account", provider='Stripe')
|
||||
|
||||
[---] text/html
|
||||
% extends "templates/layouts/settings.html"
|
||||
|
||||
% block content
|
||||
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
|
||||
<output id="stripe-errors" class="alert alert-danger"></output>
|
||||
|
||||
<form action="javascript:" method="POST" id="stripe"
|
||||
data-before-submit="call:Liberapay.stripe_before_account_submit"
|
||||
data-msg-submitting="{{ _('Request in progress, please wait…') }}"
|
||||
data-stripe-pk="{{ website.app_conf.stripe_publishable_key }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<p>{{ _(
|
||||
"Please select the country you live in or have a registered business "
|
||||
"in, and confirm that you agree to Stripe's terms."
|
||||
) }}</p>
|
||||
<div class="form-group form-inline">
|
||||
<select name="country" class="form-control country" required>
|
||||
% set country = user.guessed_country
|
||||
% if country not in constants.PAYOUT_COUNTRIES['stripe']
|
||||
<option></option>
|
||||
% endif
|
||||
% for code, name in locale.countries.items() if code in constants.PAYOUT_COUNTRIES['stripe']
|
||||
<option value="{{ code }}" {{ 'selected' if code == country }}>{{ name }}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
<p class="form-group"><label>
|
||||
<input type="checkbox" name="tos_shown_and_accepted" value="true" required />
|
||||
{{ _("I agree to the {link_start}Stripe Connected Account Agreement{link_end}.",
|
||||
link_start='<a href="https://stripe.com/connect-account/legal" target="_blank" rel="noopener noreferrer">'|safe,
|
||||
link_end='</a>'|safe) }}
|
||||
</label></p>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-primary">{{ _("Create account") }}</button>
|
||||
<a class="btn btn-default" href="{{ participant.path('payment/') }}">{{ _("Go back") }}</a>
|
||||
</div>
|
||||
</form>
|
||||
% endblock
|
||||
|
||||
% block scripts
|
||||
% do response.csp_allow_stripe()
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
% endblock
|
106
www/%username/payment/stripe/manage.spt
Normal file
106
www/%username/payment/stripe/manage.spt
Normal file
@ -0,0 +1,106 @@
|
||||
import stripe
|
||||
|
||||
from liberapay.utils import get_participant
|
||||
|
||||
[---]
|
||||
participant = get_participant(state, restrict=True, allow_member=False)
|
||||
user.require_write_permission()
|
||||
if user != participant:
|
||||
raise response.error(403)
|
||||
|
||||
payment_account = website.db.one("""
|
||||
SELECT *
|
||||
FROM payment_accounts
|
||||
WHERE participant = %s
|
||||
AND pk = %s
|
||||
""", (participant.id, request.qs.get_int('sn')))
|
||||
if not payment_account:
|
||||
raise response.error(400, "invalid `sn` value in querystring")
|
||||
|
||||
if not payment_account.details_submitted:
|
||||
account = stripe.Account.retrieve(payment_account.id)
|
||||
if account.details_submitted:
|
||||
payment_account = website.db.one("""
|
||||
UPDATE payment_accounts
|
||||
SET details_submitted = true
|
||||
, charges_enabled = %s
|
||||
, display_name = %s
|
||||
WHERE pk = %s
|
||||
RETURNING *
|
||||
""", (
|
||||
account.charges_enabled,
|
||||
account.settings.dashboard.display_name,
|
||||
payment_account.pk
|
||||
))
|
||||
else:
|
||||
response.redirect(participant.path(f'payment/stripe/onboard?sn={payment_account.pk}'))
|
||||
|
||||
if request.method == 'POST':
|
||||
allow_payout = bool(user.has_privilege('admin')) or payment_account.allow_payout
|
||||
account_session = stripe.AccountSession.create(
|
||||
account=payment_account.id,
|
||||
components={
|
||||
"account_management": {
|
||||
"enabled": True,
|
||||
"features": {
|
||||
"disable_stripe_user_authentication": True,
|
||||
},
|
||||
},
|
||||
"notification_banner": {
|
||||
"enabled": True,
|
||||
"features": {
|
||||
"disable_stripe_user_authentication": True,
|
||||
},
|
||||
},
|
||||
"payments": {
|
||||
"enabled": True,
|
||||
"features": {
|
||||
"capture_payments": False,
|
||||
"dispute_management": False,
|
||||
"refund_management": False,
|
||||
},
|
||||
},
|
||||
"payouts": {
|
||||
"enabled": True,
|
||||
"features": {
|
||||
"disable_stripe_user_authentication": True,
|
||||
"edit_payout_schedule": allow_payout,
|
||||
"instant_payouts": allow_payout,
|
||||
"standard_payouts": allow_payout,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
raise response.json({"client_secret": account_session.client_secret})
|
||||
|
||||
title = _("Manage a {provider} account", provider='Stripe')
|
||||
|
||||
[---] text/html
|
||||
% extends "templates/layouts/settings.html"
|
||||
|
||||
% block content
|
||||
<noscript><p class="alert alert-danger">{{ _("JavaScript is required") }}</p></noscript>
|
||||
<div id="stripe-notification"></div>
|
||||
<nav id="stripe-component-nav" class="hidden">
|
||||
<ul class="nav nav-pills">
|
||||
<li><a data-component="account-management" href="javascript:">{{ _("Account") }}</a></li>
|
||||
<li><a data-component="payments" href="javascript:">{{ _("Payments") }}</a></li>
|
||||
<li><a data-component="payouts" href="javascript:">{{ _("Payouts") }}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<br>
|
||||
<div id="stripe-connect" data-csrf-token="{{ csrf_token }}"
|
||||
data-msg-stripe-missing='{{ _(
|
||||
"The initialization of a required component has failed. If you use a "
|
||||
"browser extension that blocks requests, for example NoScript, please "
|
||||
"make sure it’s allowing requests to the “stripe.com” domain."
|
||||
) }}'
|
||||
data-stripe-pub-key="{{ website.app_conf.stripe_publishable_key }}"></div>
|
||||
<br><br>
|
||||
<a class="btn btn-default" href="{{ participant.path('payment') }}">{{ _("Go back") }}</a>
|
||||
% endblock
|
||||
|
||||
% block scripts
|
||||
% do response.csp_allow_stripe_connect()
|
||||
<script src="https://connect-js.stripe.com/v1.0/connect.js"></script>
|
||||
% endblock
|
61
www/%username/payment/stripe/onboard.spt
Normal file
61
www/%username/payment/stripe/onboard.spt
Normal file
@ -0,0 +1,61 @@
|
||||
import stripe
|
||||
|
||||
from liberapay.utils import get_participant
|
||||
|
||||
[---]
|
||||
participant = get_participant(state, restrict=True, allow_member=False)
|
||||
user.require_write_permission()
|
||||
if user != participant:
|
||||
raise response.error(403)
|
||||
|
||||
payment_account = website.db.one("""
|
||||
SELECT *
|
||||
FROM payment_accounts
|
||||
WHERE participant = %s
|
||||
AND pk = %s
|
||||
""", (participant.id, request.qs.get_int('sn')))
|
||||
if not payment_account:
|
||||
raise response.error(400, "invalid `sn` value in querystring")
|
||||
|
||||
account = stripe.Account.retrieve(payment_account.id)
|
||||
if account.details_submitted:
|
||||
response.redirect(participant.path(f'payment/stripe/manage?sn={payment_account.pk}'))
|
||||
|
||||
if request.method == 'POST':
|
||||
account_session = stripe.AccountSession.create(
|
||||
account=payment_account.id,
|
||||
components={
|
||||
"account_onboarding": {
|
||||
"enabled": True,
|
||||
"features": {
|
||||
"disable_stripe_user_authentication": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
raise response.json({"client_secret": account_session.client_secret})
|
||||
|
||||
title = _("Activate a {provider} account", provider='Stripe')
|
||||
|
||||
[---] text/html
|
||||
% from 'templates/macros/nav.html' import querystring_nav with context
|
||||
|
||||
% extends "templates/layouts/settings.html"
|
||||
|
||||
% block content
|
||||
<noscript><p class="alert alert-danger">{{ _("JavaScript is required") }}</p></noscript>
|
||||
<div id="stripe-connect" data-csrf-token="{{ csrf_token }}"
|
||||
data-msg-stripe-missing='{{ _(
|
||||
"The initialization of a required component has failed. If you use a "
|
||||
"browser extension that blocks requests, for example NoScript, please "
|
||||
"make sure it’s allowing requests to the “stripe.com” domain."
|
||||
) }}'
|
||||
data-stripe-pub-key="{{ website.app_conf.stripe_publishable_key }}"></div>
|
||||
<br><br>
|
||||
<a class="btn btn-default" href="{{ participant.path('payment') }}">{{ _("Go back") }}</a>
|
||||
% endblock
|
||||
|
||||
% block scripts
|
||||
% do response.csp_allow_stripe_connect()
|
||||
<script src="https://connect-js.stripe.com/v1.0/connect.js"></script>
|
||||
% endblock
|
@ -52,7 +52,7 @@ title = _("Add a payment instrument")
|
||||
<noscript><div class="alert alert-danger">{{ _("JavaScript is required") }}</div></noscript>
|
||||
|
||||
<form action="javascript:" method="POST" id="stripe"
|
||||
data-before-submit="call:Liberapay.stripe_before_submit"
|
||||
data-before-submit="call:Liberapay.stripe_before_element_submit"
|
||||
data-msg-stripe-missing='{{ _(
|
||||
"The initialization of a required component has failed. If you use a "
|
||||
"browser extension that blocks requests, for example NoScript, please "
|
||||
|
@ -130,6 +130,8 @@ elif 'state' in request.qs:
|
||||
charges_enabled=None,
|
||||
display_name=user_info['name'],
|
||||
token=token_response_data,
|
||||
independent=True,
|
||||
loss_taker='provider',
|
||||
)
|
||||
elif provider_name == 'stripe':
|
||||
data_from_stripe = token_response.json()
|
||||
@ -139,6 +141,16 @@ elif 'state' in request.qs:
|
||||
))
|
||||
account_id = data_from_stripe['stripe_user_id']
|
||||
account = stripe.Account.retrieve(account_id)
|
||||
independent = (
|
||||
account.type == 'standard' or
|
||||
account.controller.stripe_dashboard.type != "none"
|
||||
)
|
||||
assert independent
|
||||
if account.type == 'standard' or account.controller.losses.payments == 'stripe':
|
||||
loss_taker = 'provider'
|
||||
else:
|
||||
loss_taker = 'platform'
|
||||
assert loss_taker == 'provider'
|
||||
account_data.update(
|
||||
country=account.country,
|
||||
account_id=account_id,
|
||||
@ -146,6 +158,8 @@ elif 'state' in request.qs:
|
||||
charges_enabled=account.charges_enabled,
|
||||
display_name=account.settings.dashboard.display_name,
|
||||
token=None,
|
||||
independent=independent,
|
||||
loss_taker=loss_taker,
|
||||
)
|
||||
else:
|
||||
raise ValueError(provider_name)
|
||||
@ -180,10 +194,10 @@ elif 'state' in request.qs:
|
||||
INSERT INTO payment_accounts
|
||||
(participant, provider, country, id,
|
||||
default_currency, charges_enabled, verified,
|
||||
display_name, token)
|
||||
display_name, token, independent, loss_taker)
|
||||
VALUES (%(p_id)s, %(provider)s, %(country)s, %(account_id)s,
|
||||
%(default_currency)s, %(charges_enabled)s, true,
|
||||
%(display_name)s, %(token)s)
|
||||
%(display_name)s, %(token)s, %(independent)s, %(loss_taker)s)
|
||||
ON CONFLICT (provider, id, participant) DO UPDATE
|
||||
SET is_current = true
|
||||
, country = excluded.country
|
||||
@ -192,7 +206,9 @@ elif 'state' in request.qs:
|
||||
, verified = true
|
||||
, authorized = true
|
||||
, display_name = excluded.display_name
|
||||
, token = excluded.token;
|
||||
, token = excluded.token
|
||||
, independent = excluded.independent
|
||||
, loss_taker = excluded.loss_taker;
|
||||
""", account_data)
|
||||
|
||||
response.erase_cookie(cookie_name)
|
||||
|
@ -32,15 +32,19 @@ if request.method == 'POST':
|
||||
AND country = %(country)s;
|
||||
|
||||
INSERT INTO payment_accounts
|
||||
(participant, provider, country, id, verified)
|
||||
VALUES (%(p_id)s, 'paypal', %(country)s, %(account_id)s, %(verified)s)
|
||||
(participant, provider, country, id, verified,
|
||||
independent, loss_taker)
|
||||
VALUES (%(p_id)s, 'paypal', %(country)s, %(account_id)s, %(verified)s,
|
||||
true, 'provider')
|
||||
ON CONFLICT (provider, id, participant) DO UPDATE
|
||||
SET is_current = true
|
||||
, country = excluded.country
|
||||
, default_currency = excluded.default_currency
|
||||
, charges_enabled = excluded.charges_enabled
|
||||
, display_name = excluded.display_name
|
||||
, token = excluded.token;
|
||||
, token = excluded.token
|
||||
, independent = excluded.independent
|
||||
, loss_taker = excluded.loss_taker;
|
||||
""", account_data)
|
||||
response.redirect(participant.path('payment'))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user