Skip to content

Commit 32d3715

Browse files
Softlaunch new checkout
1 parent 3c21f1a commit 32d3715

File tree

20 files changed

+1177
-318
lines changed

20 files changed

+1177
-318
lines changed

assets/css/site/dialog.css

+14-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ body:has(.dialog[open]) {
1515
overflow: hidden;
1616
}
1717

18+
.dialog-form {
19+
position: relative;
20+
accent-color: var(--color-blue-700);
21+
}
22+
1823
.dialog-form .field+.field {
1924
margin-top: var(--spacing-6);
2025
}
@@ -25,14 +30,14 @@ body:has(.dialog[open]) {
2530
margin-bottom: var(--spacing-2);
2631
}
2732

28-
.dialog-form .label abbr {
33+
.dialog-form label abbr {
2934
text-decoration: none;
30-
color: var(--color-red-500);
35+
color: var(--color-red-600);
3136
margin-left: .125rem;
3237
display: none;
3338
}
3439

35-
.dialog-form .field:has(*:invalid) .label abbr {
40+
.dialog-form .field:has(*:invalid) label abbr {
3641
display: inline;
3742
}
3843

@@ -43,13 +48,17 @@ body:has(.dialog[open]) {
4348
border-radius: var(--rounded-sm);
4449
box-shadow: 0px 0px 0px 1px var(--color-border);
4550
}
51+
.dialog-form :where(input:not([type=checkbox], [type=radio]), select):focus {
52+
outline: 2px solid var(--color-blue-700);
53+
}
4654

47-
.dialog-form select.input {
55+
.dialog-form select {
4856
appearance: none;
57+
font-size: inherit;
4958
}
5059

5160
.dialog-form .checkbox {
52-
height: 2.25rem;
61+
min-height: 2.25rem;
5362
display: flex;
5463
align-items: center;
5564
color: var(--color-black);
@@ -71,4 +80,3 @@ body:has(.dialog[open]) {
7180
flex-basis: 50%;
7281
flex-grow: 1;
7382
}
74-

assets/css/site/icons.css

+10
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@
77
width: 18px;
88
height: 18px;
99
}
10+
11+
svg[data-type="loader"] {
12+
animation: Spin 1.5s linear infinite;
13+
}
14+
15+
@keyframes Spin {
16+
100% {
17+
transform: rotate(360deg);
18+
}
19+
}

content/buy/answers/3_revenue-limit/answer.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Title: What does the **revenue limit for Kirby Basic** mean?
22
----
33
Text:
44

5-
The **Basic license** at its reduced price is only available for projects of individuals, companies and organizations that don't exceed a **yearly revenue or funding of € 1 million**.
5+
The **Basic license** at its reduced price is only available for projects of individuals, companies and organizations that don't exceed a **yearly revenue or funding of (revenue-limit: verbose)**.
66

77
If you exceed this limit, you need to purchase an **Enterprise license** at regular price which features **no revenue limit**.
88

site/config/buy.php

+14-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@
99
'product' => 824340,
1010
'regular' => 349,
1111
],
12+
'revenueLimit' => 1000000,
1213
'sale' => [
13-
'start' => '2023-11-28',
14-
'end' => '2023-12-18',
14+
'start' => '2023-02-22',
15+
'end' => '2023-02-24',
1516
'discount' => 20
1617
],
18+
'quantities' => [
19+
'min' => 1,
20+
'max' => 100,
21+
],
1722
'volume' => [
1823
5 => 5,
1924
10 => 10,
2025
15 => 15
26+
],
27+
'donation' => [
28+
'teamAmount' => 0,
29+
'customerAmount' => 0,
30+
'charity' => 'Amadeu Antonio Foundation',
31+
'purpose' => 'for their work on reinforcing a democratic civil society',
32+
'link' => 'https://www.amadeu-antonio-stiftung.de/en/'
2133
]
2234
];

site/config/routes.php

+121-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Buy\Paddle;
4+
use Buy\Passthrough;
45
use Buy\Product;
56
use Kirby\Cms\Page;
67

@@ -49,37 +50,119 @@
4950
[
5051
'pattern' => 'buy/prices',
5152
'action' => function () {
52-
// uncomment to test a specific country
53-
// Buy\Paddle::visitor(country: 'US');
54-
5553
$basic = Product::Basic;
5654
$enterprise = Product::Enterprise;
57-
$visitor = Paddle::visitor();
55+
$visitor = Paddle::visitor(country: get('country'));
5856

5957
return json_encode([
60-
'basic-regular' => $basic->price()->regular(),
61-
'basic-sale' => $basic->price()->sale(),
62-
'enterprise-regular' => $enterprise->price()->regular(),
63-
'enterprise-sale' => $enterprise->price()->sale(),
64-
'currency-sign' => $visitor->currencySign(),
65-
'currency-sign-trimmed' => rtrim($visitor->currencySign(), ' '),
66-
'revenue-limit' => $visitor->currency() !== 'EUR' ? ' (' . $visitor->revenueLimit(1000000) . ')' : '',
67-
'status' => $visitor->error() ?? 'OK'
58+
'status' => $visitor->error() ?? 'OK',
59+
'country' => $visitor->country(),
60+
'currency' => $visitor->currencySign(),
61+
'prices' => [
62+
'basic' => [
63+
'regular' => $basic->price()->regular(),
64+
'sale' => $basic->price()->sale()
65+
],
66+
'donation' => [
67+
'customer' => $basic->price()->customerDonation(),
68+
'team' => $basic->price()->teamDonation(),
69+
],
70+
'enterprise' => [
71+
'regular' => $enterprise->price()->regular(),
72+
'sale' => $enterprise->price()->sale()
73+
],
74+
],
75+
'revenueLimit' => $visitor->currency() !== 'EUR' ? ' (' . $visitor->revenueLimit() . ')' : '',
76+
'vatRate' => $visitor->vatRate() ?? 0,
6877
], JSON_UNESCAPED_UNICODE);
6978
}
7079
],
80+
[
81+
'pattern' => 'buy',
82+
'method' => 'POST',
83+
'action' => function () {
84+
$city = get('city');
85+
$company = get('company');
86+
$country = get('country');
87+
$donate = get('donate') === 'on';
88+
$email = get('email');
89+
$newsletter = get('newsletter') === 'on';
90+
$productId = get('product');
91+
$postalCode = get('postalCode');
92+
$state = get('state');
93+
$street = get('street');
94+
$quantity = Product::restrictQuantity(get('quantity', 1));
95+
$vatId = get('vatId');
96+
97+
try {
98+
// use the provided country for the calculation, not the IP address
99+
Paddle::visitor(country: $country);
100+
101+
$product = Product::from($productId);
102+
$price = $product->price();
103+
$message = $product->revenueLimit();
104+
$passthrough = new Passthrough(teamDonation: option('buy.donation.teamAmount') * $quantity);
105+
106+
$eurPrice = $product->price('EUR')->volume($quantity);
107+
$localizedPrice = $price->volume($quantity);
108+
109+
if ($donate === true) {
110+
// prices per license
111+
$customerDonation = option('buy.donation.customerAmount');
112+
$eurPrice += $customerDonation;
113+
$localizedPrice += $price->convert($customerDonation);
114+
115+
// donation overall
116+
$customerDonation *= $quantity;
117+
$passthrough->customerDonation = $customerDonation;
118+
119+
$message .= ' We will donate an additional €' . $customerDonation . ' to ' . option('buy.donation.charity') . '. Thank you for your donation!';
120+
}
121+
122+
$prices = [
123+
'EUR:' . $eurPrice,
124+
$price->currency . ':' . $localizedPrice,
125+
];
126+
127+
go($product->checkout('buy', [
128+
'custom_message' => $message,
129+
'customer_country' => $country,
130+
'customer_email' => $email,
131+
'customer_postcode' => $postalCode,
132+
'marketing_consent' => $newsletter ? 1 : 0,
133+
'passthrough' => $passthrough,
134+
'prices' => $prices,
135+
'quantity' => $quantity,
136+
'vat_city' => $city,
137+
'vat_country' => $country,
138+
'vat_company_name' => $company,
139+
'vat_number' => $vatId,
140+
'vat_postcode' => $postalCode,
141+
'vat_state' => $state,
142+
'vat_street' => $street,
143+
]));
144+
} catch (Throwable $e) {
145+
die($e->getMessage() . '<br>Please contact us: [email protected]');
146+
}
147+
},
148+
],
71149
[
72150
'pattern' => 'buy/(enterprise|basic)',
73-
'action' => function (string $product) {
151+
'action' => function (string $productId) {
74152
try {
75-
$product = Product::from($product);
76-
$price = $product->price();
153+
$product = Product::from($productId);
154+
$price = $product->price();
155+
$passthrough = new Passthrough(teamDonation: option('buy.donation.teamAmount'));
156+
157+
$eurPrice = $product->price('EUR')->sale();
158+
$localizedPrice = $price->sale();
159+
77160
$prices = [
78-
'EUR:' . $product->price('EUR')->sale(),
79-
$price->currency . ':' . $price->sale(),
161+
'EUR:' . $eurPrice,
162+
$price->currency . ':' . $localizedPrice,
80163
];
81164

82-
go($product->checkout('buy', compact('prices')));
165+
go($product->checkout('buy', compact('prices', 'passthrough')));
83166
} catch (Throwable $e) {
84167
die($e->getMessage() . '<br>Please contact us: [email protected]');
85168
}
@@ -89,35 +172,44 @@
89172
'pattern' => 'buy/volume',
90173
'method' => 'POST',
91174
'action' => function () {
92-
$product = get('product', 'basic');
93-
$quantity = get('volume', 5);
175+
$productId = get('product', 'basic');
176+
$quantity = Product::restrictQuantity(get('volume', 5));
94177

95178
try {
96-
$product = Product::from($product);
97-
$price = $product->price();
179+
$product = Product::from($productId);
180+
$price = $product->price();
181+
$passthrough = new Passthrough(teamDonation: option('buy.donation.teamAmount') * $quantity);
182+
183+
$eurPrice = $product->price('EUR')->volume($quantity);
184+
$localizedPrice = $price->volume($quantity);
185+
98186
$prices = [
99-
'EUR:' . $product->price('EUR')->volume($quantity),
100-
$price->currency . ':' . $price->volume($quantity),
187+
'EUR:' . $eurPrice,
188+
$price->currency . ':' . $localizedPrice,
101189
];
102190

103-
go($product->checkout('buy', compact('prices', 'quantity')));
191+
go($product->checkout('buy', compact('prices', 'quantity', 'passthrough')));
104192
} catch (Throwable $e) {
105193
die($e->getMessage() . '<br>Please contact us: [email protected]');
106194
}
107195
}
108196
],
109197
[
110198
'pattern' => 'buy/volume/(enterprise|basic)/(:num)',
111-
'action' => function (string $product, int $quantity) {
199+
'action' => function (string $productId, int $quantity) {
200+
$quantity = Product::restrictQuantity($quantity);
201+
112202
try {
113-
$product = Product::from($product);
114-
$price = $product->price();
203+
$product = Product::from($productId);
204+
$price = $product->price();
205+
$passthrough = new Passthrough(teamDonation: option('buy.donation.teamAmount') * $quantity);
206+
115207
$prices = [
116208
'EUR:' . $product->price('EUR')->volume($quantity),
117209
$price->currency . ':' . $price->volume($quantity),
118210
];
119211

120-
go($product->checkout('buy', compact('prices', 'quantity')));
212+
go($product->checkout('buy', compact('prices', 'quantity', 'passthrough')));
121213
} catch (Throwable $e) {
122214
die($e->getMessage() . '<br>Please contact us: [email protected]');
123215
}

site/controllers/buy.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@
88
// expire the cache when the sale banner/prices change
99
$sale->expires();
1010

11+
$discounts = option('buy.volume');
12+
$discountsReversed = $discounts;
13+
krsort($discountsReversed);
14+
1115
return [
12-
'discounts' => option('buy.volume'),
13-
'sale' => $sale,
14-
'questions' => $page->find('answers')->children()
16+
'basic' => Buy\Product::Basic,
17+
'countries' => option('countries'),
18+
'discounts' => $discounts,
19+
'discountsReversed' => $discountsReversed,
20+
'donation' => option('buy.donation'),
21+
'enterprise' => Buy\Product::Enterprise,
22+
'sale' => $sale,
23+
'questions' => $page->find('answers')->children(),
24+
'revenueLimitShort' => Buy\RevenueLimit::approximation(),
25+
'revenueLimitVerbose' => Buy\RevenueLimit::approximation(verbose: true),
1526
];
1627
};

site/plugins/buy/Paddle.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Exception;
66
use Kirby\Http\Remote;
7+
use Kirby\Toolkit\Str;
78
use Throwable;
89

910
class Paddle
@@ -28,6 +29,9 @@ public static function checkout(string $product, array $payload = []): string
2829
...$payload
2930
];
3031

32+
// normalize the passthrough param to a JSON string
33+
$data['passthrough'] = Passthrough::factory($data['passthrough'] ?? null)->toJson();
34+
3135
$response = static::request(
3236
endpoint: 'product/generate_pay_link',
3337
method: 'POST',
@@ -122,7 +126,10 @@ public static function visitor(string|null $country = null): Visitor
122126

123127
// calculate conversion rate from the EUR price;
124128
// requires that the EUR price matches between the site and Paddle admin
125-
rate: $paddleProduct['list_price']['net'] / $product->rawPrice()
129+
rate: $paddleProduct['list_price']['net'] / $product->rawPrice(),
130+
131+
// calculate VAT rate, rounded to four decimal places to avoid float mishaps
132+
vatRate: round($paddleProduct['list_price']['tax'] / $paddleProduct['list_price']['net'] * 10000) / 10000,
126133
);
127134
} catch (Throwable $e) {
128135
// on any kind of error, use the EUR prices as a fallback

0 commit comments

Comments
 (0)