Skip to content

Commit e23ec69

Browse files
committed
Merge branch 'develop' into feature/workshop-banner
# Conflicts: # layouts/_default/baseof.html
2 parents 52fddf4 + 2090db7 commit e23ec69

File tree

75 files changed

+3206
-843
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+3206
-843
lines changed

assets/js/androidkey.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,20 @@ class AndroidLicense {
4141
}).catch(() => {
4242
this._checkoutData.errorMessage = 'Retrieving price failed. Please try again later.';
4343
});
44-
// let discountedRequest = {
45-
// items: [{ priceId: PADDLE_ANDROID_PRICE_ID, quantity: 1}],
46-
// discountId: PADDLE_DISCOUNT_ID
47-
// };
48-
// paddle.PricePreview(discountedRequest).then(discountedResult => {
49-
// if (Number(discountedResult.data.details.lineItems[0].totals.discount) > 0) {
50-
// this._checkoutData.discountedPrice = {
51-
// amount: discountedResult.data.details.lineItems[0].totals.total,
52-
// formattedAmount: discountedResult.data.details.lineItems[0].formattedTotals.total
53-
// };
54-
// }
55-
// }).catch(error => {
56-
// this._checkoutData.errorMessage = 'Retrieving discounted price failed. Please try again later.';
57-
// });
44+
if (PADDLE_ANDROID_SALE_PRICE_ID) {
45+
let saleRequest = {
46+
items: [{ priceId: PADDLE_ANDROID_SALE_PRICE_ID, quantity: 1 }]
47+
};
48+
paddle.PricePreview(saleRequest).then(saleResult => {
49+
this._checkoutData.discountedPrice = {
50+
priceId: PADDLE_ANDROID_SALE_PRICE_ID,
51+
amount: saleResult.data.details.lineItems[0].totals.total,
52+
formattedAmount: saleResult.data.details.lineItems[0].formattedTotals.total
53+
};
54+
}).catch(() => {
55+
// sale price not available, proceed with regular price
56+
});
57+
}
5858
});
5959
}
6060

@@ -73,10 +73,11 @@ class AndroidLicense {
7373
this._checkoutData.inProgress = true;
7474
this._checkoutData.errorMessage = '';
7575
this._checkoutData.success = false;
76+
let checkoutPriceId = this._checkoutData.discountedPrice ? this._checkoutData.discountedPrice.priceId : PADDLE_ANDROID_PRICE_ID;
7677
this._paddle.then(paddle => {
7778
paddle.Checkout.open({
7879
settings: { locale: locale },
79-
items: [{ priceId: PADDLE_ANDROID_PRICE_ID, quantity: 1 }],
80+
items: [{ priceId: checkoutPriceId, quantity: 1 }],
8081
customer: { email: this._checkoutData.email }
8182
});
8283
});

assets/js/const.template.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ const PADDLE_TOKEN = '{{ .Site.Params.paddleToken }}';
66
const PADDLE_VENDOR_ID = {{ .Site.Params.paddleVendorId }};
77
const PADDLE_DESKTOP_PRICE_IDS = {{ .Site.Params.paddleDesktopPriceIds | jsonify }};
88
const PADDLE_ANDROID_PRICE_ID = '{{ .Site.Params.paddleAndroidPriceId }}';
9-
const PADDLE_HUB_SELF_HOSTED_SUBSCRIPTION_PLAN_ID = {{ .Site.Params.paddleHubSelfHostedSubscriptionPlanId }};
10-
const PADDLE_HUB_MANAGED_SUBSCRIPTION_PLAN_ID = {{ .Site.Params.paddleHubManagedSubscriptionPlanId }};
9+
const PADDLE_HUB_SELF_HOSTED_YEARLY_PLAN_ID = {{ .Site.Params.paddleHubSelfHostedYearlyPlanId }};
10+
const PADDLE_HUB_SELF_HOSTED_MONTHLY_PLAN_ID = {{ .Site.Params.paddleHubSelfHostedMonthlyPlanId }};
11+
const PADDLE_HUB_MANAGED_YEARLY_PLAN_ID = {{ .Site.Params.paddleHubManagedYearlyPlanId }};
12+
const PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID = {{ .Site.Params.paddleHubManagedMonthlyPlanId }};
1113
const PADDLE_PRICES_URL = '{{ .Site.Params.paddlePricesUrl }}';
12-
const PADDLE_DISCOUNT_ID = '{{ .Site.Params.paddleDiscountId }}';
13-
const PADDLE_DISCOUNT_CODE = '{{ .Site.Params.paddleDiscountCode }}';
14+
const PADDLE_DESKTOP_SALE_PRICE_ID = '{{ .Site.Params.paddleDesktopSalePriceId }}';
15+
const PADDLE_ANDROID_SALE_PRICE_ID = '{{ .Site.Params.paddleAndroidSalePriceId }}';
1416
const LEGACY_STORE_URL = '{{ .Site.Params.legacyStoreUrl }}';
17+
const HUB_MANAGED_DOMAIN = '{{ .Site.Params.hubManagedDomain }}';
1518
const STRIPE_PK = '{{ .Site.Params.stripePk }}';

assets/js/desktopkey.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ class DesktopLicense {
4444
}).catch(() => {
4545
this._checkoutData.errorMessage = 'Retrieving prices failed. Please try again later.';
4646
});
47-
// let discountedRequest = {
48-
// items: [{ priceId: PADDLE_DESKTOP_PRICE_IDS[0], quantity: 1 }],
49-
// discountId: PADDLE_DISCOUNT_ID
50-
// };
51-
// paddle.PricePreview(discountedRequest).then(discountedResult => {
52-
// if (Number(discountedResult.data.details.lineItems[0].totals.discount) > 0) {
53-
// this._checkoutData.discountedPrice = {
54-
// amount: discountedResult.data.details.lineItems[0].totals.total,
55-
// formattedAmount: discountedResult.data.details.lineItems[0].formattedTotals.total
56-
// };
57-
// }
58-
// }).catch(() => {
59-
// this._checkoutData.errorMessage = 'Retrieving discounted price failed. Please try again later.';
60-
// });
47+
if (PADDLE_DESKTOP_SALE_PRICE_ID) {
48+
let saleRequest = {
49+
items: [{ priceId: PADDLE_DESKTOP_SALE_PRICE_ID, quantity: 1 }]
50+
};
51+
paddle.PricePreview(saleRequest).then(saleResult => {
52+
this._checkoutData.discountedPrice = {
53+
priceId: PADDLE_DESKTOP_SALE_PRICE_ID,
54+
amount: saleResult.data.details.lineItems[0].totals.total,
55+
formattedAmount: saleResult.data.details.lineItems[0].formattedTotals.total
56+
};
57+
}).catch(() => {
58+
// sale price not available, proceed with regular price
59+
});
60+
}
6161
});
6262
}
6363

@@ -76,10 +76,11 @@ class DesktopLicense {
7676
this._checkoutData.inProgress = true;
7777
this._checkoutData.errorMessage = '';
7878
this._checkoutData.success = false;
79+
let checkoutPriceId = (priceId === PADDLE_DESKTOP_PRICE_IDS[0] && this._checkoutData.discountedPrice) ? this._checkoutData.discountedPrice.priceId : priceId;
7980
this._paddle.then(paddle => {
8081
paddle.Checkout.open({
8182
settings: { locale: locale },
82-
items: [{ priceId: priceId, quantity: Number.parseInt(this._checkoutData.quantity) }],
83+
items: [{ priceId: checkoutPriceId, quantity: Number.parseInt(this._checkoutData.quantity) }],
8384
customer: { email: this._checkoutData.email }
8485
});
8586
});

assets/js/download.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,65 @@
1+
/**
2+
* Detects if the current Mac is running Apple Silicon or Intel.
3+
* Uses WebGL GPU renderer detection.
4+
* @returns {'apple-silicon' | 'intel' | 'unknown'}
5+
*/
6+
function detectMacArchitecture() {
7+
if (navigator.appVersion.indexOf('Mac') === -1) {
8+
return 'unknown';
9+
}
10+
11+
try {
12+
const canvas = document.createElement('canvas');
13+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
14+
if (!gl) {
15+
return 'unknown';
16+
}
17+
18+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
19+
if (debugInfo) {
20+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';
21+
const rendererLower = renderer.toLowerCase();
22+
23+
// Apple Silicon Macs have Apple GPUs, Intel Macs have Intel/AMD GPUs
24+
if (rendererLower.includes('apple')) {
25+
return 'apple-silicon';
26+
}
27+
28+
// Intel or AMD GPUs indicate Intel Mac
29+
if (rendererLower.includes('intel') || rendererLower.includes('amd') || rendererLower.includes('radeon')) {
30+
return 'intel';
31+
}
32+
}
33+
} catch (e) {
34+
// Detection failed, fall through to unknown
35+
}
36+
37+
return 'unknown';
38+
}
39+
40+
/**
41+
* Detects Linux architecture from navigator.platform.
42+
* @returns {'x86_64' | 'aarch64' | 'unknown'}
43+
*/
44+
function detectLinuxArchitecture() {
45+
const platform = navigator.platform.toLowerCase();
46+
47+
if (!platform.includes('linux')) {
48+
return 'unknown';
49+
}
50+
51+
if (platform.includes('aarch64') || platform.includes('arm')) {
52+
return 'aarch64';
53+
}
54+
55+
if (platform.includes('x86_64') || platform.includes('x86-64') || platform.includes('amd64')) {
56+
return 'x86_64';
57+
}
58+
59+
// Most Linux desktops are x86_64
60+
return 'x86_64';
61+
}
62+
163
function guessDownloadTab(locationHash) {
264
if (locationHash === '#win' || locationHash === '#mac' || locationHash === '#linux' || locationHash === '#android' || locationHash === '#ios') {
365
return locationHash;

assets/js/hubce.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use strict";
2+
3+
// requires newsletter.js
4+
const VERIFY_EMAIL_URL = API_BASE_URL + '/connect/email/verify';
5+
const REFRESH_LICENSE_URL = API_BASE_URL + '/licenses/hub/refresh';
6+
7+
class HubCE {
8+
9+
constructor(form, feedbackData, submitData, searchParams) {
10+
this._form = form;
11+
this._feedbackData = feedbackData;
12+
this._submitData = submitData;
13+
this._searchParams = searchParams;
14+
this._submitData.oldLicense = searchParams.get('oldLicense');
15+
this._submitData.returnUrl = searchParams.get('returnUrl');
16+
17+
// continue after email verified:
18+
if (searchParams.get('verifiedEmail')) {
19+
feedbackData.currentStep = 1;
20+
feedbackData.emailVerified = true;
21+
}
22+
}
23+
24+
submit() {
25+
if (this._feedbackData.currentStep === 0) {
26+
this.validateEmail();
27+
} else if (this._feedbackData.currentStep === 1) {
28+
this.sendConfirmationEmail();
29+
} else if (this._feedbackData.currentStep === 2) {
30+
this.getHubLicense();
31+
}
32+
}
33+
34+
validateEmail() {
35+
if (!$(this._form)[0].checkValidity()) {
36+
$(this._form).find(':input').addClass('show-invalid');
37+
this._feedbackData.errorMessage = 'Please fill in all required fields.';
38+
return;
39+
}
40+
this.onValidationSucceeded();
41+
}
42+
43+
onValidationFailed(error) {
44+
this._feedbackData.inProgress = false;
45+
this._feedbackData.errorMessage = error;
46+
}
47+
48+
onValidationSucceeded() {
49+
this._feedbackData.currentStep++;
50+
this._feedbackData.inProgress = false;
51+
this._feedbackData.errorMessage = '';
52+
}
53+
54+
sendConfirmationEmail() {
55+
if (!$(this._form)[0].checkValidity()) {
56+
$(this._form).find(':input').addClass('show-invalid');
57+
this._feedbackData.errorMessage = 'Please fill in all required fields.';
58+
return;
59+
}
60+
61+
this._feedbackData.success = false;
62+
this._feedbackData.inProgress = true;
63+
this._feedbackData.errorMessage = '';
64+
$.ajax({
65+
url: VERIFY_EMAIL_URL,
66+
type: 'POST',
67+
data: {
68+
email: this._submitData.email,
69+
oldLicense: this._submitData.oldLicense,
70+
returnUrl: this._submitData.returnUrl,
71+
verifyCaptcha: this._submitData.captcha,
72+
verifyEmail: this._submitData.email,
73+
verifyTarget: 'registerhubce'
74+
}
75+
}).done(_ => {
76+
this.onRequestSucceeded();
77+
if (this._submitData.acceptNewsletter) {
78+
subscribeToNewsletter(this._submitData.email, 7); // FIXME move to backend
79+
}
80+
}).fail(xhr => {
81+
this.onRequestFailed(xhr.responseJSON?.message || 'Sending confirmation email failed.');
82+
});
83+
}
84+
85+
getHubLicense() {
86+
this._feedbackData.inProgress = true;
87+
this._feedbackData.errorMessage = '';
88+
$.ajax({
89+
url: REFRESH_LICENSE_URL,
90+
type: 'POST',
91+
data: {
92+
token: this._submitData.oldLicense,
93+
captcha: this._submitData.captcha
94+
}
95+
}).done(response => {
96+
this._feedbackData.licenseText = response;
97+
this._feedbackData.inProgress = false;
98+
}).fail(xhr => {
99+
this.onRequestFailed(xhr.responseJSON?.message || 'Fetching license failed.');
100+
});
101+
}
102+
103+
onRequestFailed(error) {
104+
this._feedbackData.inProgress = false;
105+
this._feedbackData.errorMessage = error;
106+
}
107+
108+
onRequestSucceeded() {
109+
this._feedbackData.emailSent = true;
110+
this._feedbackData.inProgress = false;
111+
this._feedbackData.errorMessage = '';
112+
}
113+
114+
}

assets/js/hubmanaged.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ class HubManaged {
134134
}
135135

136136
function subdomainToURL(subdomain) {
137-
return `https://${subdomain}.cryptomator.cloud`;
137+
return `https://${subdomain}.${HUB_MANAGED_DOMAIN}`;
138138
}

assets/js/hubpricing.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,43 @@ class HubPricing {
66
this._data = data;
77
}
88

9-
loadPrice() {
9+
loadPrices() {
1010
$.ajax({
1111
url: PADDLE_PRICES_URL,
1212
dataType: 'jsonp',
1313
data: {
14-
product_ids: `${PADDLE_HUB_SELF_HOSTED_SUBSCRIPTION_PLAN_ID},${PADDLE_HUB_MANAGED_SUBSCRIPTION_PLAN_ID}`
14+
product_ids: `${PADDLE_HUB_SELF_HOSTED_YEARLY_PLAN_ID},${PADDLE_HUB_SELF_HOSTED_MONTHLY_PLAN_ID},${PADDLE_HUB_MANAGED_YEARLY_PLAN_ID},${PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID}`
1515
},
1616
}).done(data => {
17-
this._data.selfHostedMonthlyPrice = {
18-
amount: data.response.products[0].subscription.price.net / 12,
19-
currency: data.response.products[0].currency
20-
};
21-
this._data.managedMonthlyPrice = {
22-
amount: data.response.products[1].subscription.price.net / 12,
23-
currency: data.response.products[1].currency
24-
};
17+
const priceMap = {};
18+
data.response.products.forEach(p => {
19+
priceMap[p.product_id] = {
20+
amount: p.subscription.price.net,
21+
currency: p.currency
22+
};
23+
});
24+
25+
this._assignPrice(priceMap, PADDLE_HUB_SELF_HOSTED_YEARLY_PLAN_ID, 'selfHostedYearlyPrice', 12);
26+
this._assignPrice(priceMap, PADDLE_HUB_SELF_HOSTED_MONTHLY_PLAN_ID, 'selfHostedMonthlyPrice', 1);
27+
this._assignPrice(priceMap, PADDLE_HUB_MANAGED_YEARLY_PLAN_ID, 'managedYearlyPrice', 12);
28+
this._assignPrice(priceMap, PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID, 'managedMonthlyPrice', 1);
29+
30+
const mYearly = priceMap[PADDLE_HUB_MANAGED_YEARLY_PLAN_ID];
31+
const mMonthly = priceMap[PADDLE_HUB_MANAGED_MONTHLY_PLAN_ID];
32+
if (mYearly && mMonthly) {
33+
this._data.savingsPercent = Math.max(0, Math.round((1 - mYearly.amount / (mMonthly.amount * 12)) * 100));
34+
}
2535
});
2636
}
2737

38+
_assignPrice(priceMap, planId, dataKey, divisor) {
39+
const price = priceMap[planId];
40+
if (price) {
41+
this._data[dataKey] = {
42+
amount: price.amount / divisor,
43+
currency: price.currency
44+
};
45+
}
46+
}
47+
2848
}

assets/js/hubsetup.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ EOF`;
428428
'init-config': {condition: 'service_completed_successfully'},
429429
'postgres': {condition: 'service_healthy'}
430430
},
431-
image: 'ghcr.io/cryptomator/keycloak:26.5.2',
431+
image: 'ghcr.io/cryptomator/keycloak:26.5.6',
432432
command: startCmd,
433433
volumes: ['kc-config:/opt/keycloak/data/import'],
434434
deploy: {
@@ -799,7 +799,7 @@ class KubernetesConfigBuilder extends ConfigBuilder {
799799
}],
800800
containers: [{
801801
name: 'keycloak',
802-
image: 'ghcr.io/cryptomator/keycloak:26.5.2',
802+
image: 'ghcr.io/cryptomator/keycloak:26.5.6',
803803
command: startCmd,
804804
ports: [{containerPort: 8080}],
805805
resources: {

0 commit comments

Comments
 (0)