feat(billingplan): Add Paddle v2 checkout support and backend config endpoint; add CSP headers and bump typedserver

This commit is contained in:
2025-12-07 20:45:30 +00:00
parent 9d9f90c1d5
commit 2cdf86744e
9 changed files with 111 additions and 40 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## 2025-12-07 - 1.10.0 - feat(billingplan)
Add Paddle v2 checkout support and backend config endpoint; add CSP headers and bump typedserver
- Add getPaddleConfig typedrequest handler in BillingPlanManager to expose PADDLE_TOKEN and PADDLE_PRICE_ID from environment.
- Introduce IReq_GetPaddleConfig typedrequest interface.
- Update frontend paddlesetup to use Paddle v2: load v2 script, call Paddle.Initialize with token, open Checkout using items.priceId and customer.email, and handle checkout.completed events (store transaction_id).
- Attempt to obtain user email from account state or via idpClient.whoIs before starting checkout; show error if email unavailable.
- Add Content Security Policy securityHeaders to website server configuration to allow Paddle, ProfitWell, Sentry and related assets/connections.
- Bump dependency @api.global/typedserver from ^7.8.17 to ^7.10.2.
## 2025-12-01 - 1.9.0 - feat(account)
Refactor account UI: migrate modals to promise-based show() API and improve navigation URL tracking
+1 -1
View File
@@ -18,7 +18,7 @@
"dependencies": {
"@api.global/typedrequest": "^3.2.5",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^7.8.17",
"@api.global/typedserver": "^7.10.2",
"@api.global/typedsocket": "^4.1.0",
"@consent.software/catalog": "^2.0.1",
"@design.estate/dees-catalog": "^2.0.3",
+27 -27
View File
@@ -15,11 +15,11 @@ importers:
specifier: ^3.0.19
version: 3.0.19
'@api.global/typedserver':
specifier: ^7.8.17
version: 7.8.17(@tiptap/pm@2.27.1)
specifier: ^7.10.2
version: 7.10.2(@tiptap/pm@2.27.1)
'@api.global/typedsocket':
specifier: ^4.1.0
version: 4.1.0(@push.rocks/smartserve@1.1.2)
version: 4.1.0(@push.rocks/smartserve@1.3.0)
'@consent.software/catalog':
specifier: ^2.0.1
version: 2.0.1
@@ -94,7 +94,7 @@ importers:
version: 2.0.20
'@serve.zone/platformclient':
specifier: ^1.1.2
version: 1.1.2(@push.rocks/smartserve@1.1.2)
version: 1.1.2(@push.rocks/smartserve@1.3.0)
'@tsclass/tsclass':
specifier: ^9.3.0
version: 9.3.0
@@ -113,7 +113,7 @@ importers:
version: 2.0.0
'@git.zone/tswatch':
specifier: ^2.2.3
version: 2.2.3(@push.rocks/smartserve@1.1.2)
version: 2.2.3(@push.rocks/smartserve@1.3.0)
'@push.rocks/projectinfo':
specifier: ^5.0.1
version: 5.0.2
@@ -135,8 +135,8 @@ packages:
'@api.global/typedserver@3.0.80':
resolution: {integrity: sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==}
'@api.global/typedserver@7.8.17':
resolution: {integrity: sha512-I4aA8l2f43HP70dZTEqOyWiDcolFsZm6/0d3B7lYq/DwzorV4IJZ0ZO18jHyGHZuaaJri4m5j3Wt7dn6yHDY3A==}
'@api.global/typedserver@7.10.2':
resolution: {integrity: sha512-b/LzAbcJB6aPTd044s23yptZEukD7Hvsi8mXfPfE68OI85I2vfalFO8/h0E9LizoeDFGaTguLXhZYnGl8HkVZQ==}
'@api.global/typedsocket@3.1.1':
resolution: {integrity: sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==}
@@ -911,8 +911,8 @@ packages:
'@push.rocks/smartrx@3.0.10':
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
'@push.rocks/smartserve@1.1.2':
resolution: {integrity: sha512-NkJNgdDt/rfsd9AMheCxtFd5X+ubzffvxOxjb0Aw1A5JR3xmiWeRifqEV1oN7mMTGL9jyQVvIME6Yrdxr244dA==}
'@push.rocks/smartserve@1.3.0':
resolution: {integrity: sha512-4ZR9uKVWXVAPzU5wtCQ1mA9jNmOlUl3oGr50EceLT6803UwbNcst7Ek/BhzSaZ0qb2pz0jO5T/V+icgvZ1/5ww==}
'@push.rocks/smartshell@3.3.0':
resolution: {integrity: sha512-m0w618H6YBs+vXGz1CgS4nPi5CUAnqRtckcS9/koGwfcIx1IpjqmiP47BoCTbdgcv0IPUxQVBG1IXTHPuZ8Z5g==}
@@ -3678,11 +3678,11 @@ snapshots:
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstream': 1.0.10
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@1.1.2)':
'@api.global/typedserver@3.0.80(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.1.2)
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.3.0)
'@cloudflare/workers-types': 4.20251202.0
'@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2
@@ -3726,11 +3726,11 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedserver@7.8.17(@tiptap/pm@2.27.1)':
'@api.global/typedserver@7.10.2(@tiptap/pm@2.27.1)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.1.2)
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@1.3.0)
'@cloudflare/workers-types': 4.20251205.0
'@design.estate/dees-catalog': 2.0.3(@tiptap/pm@2.27.1)
'@design.estate/dees-comms': 1.0.30
@@ -3753,7 +3753,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 5.0.1
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartserve': 1.1.2
'@push.rocks/smartserve': 1.3.0
'@push.rocks/smartsitemap': 2.0.4
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smarttime': 4.1.1
@@ -3772,18 +3772,18 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@1.1.2)':
'@api.global/typedsocket@3.1.1(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartsocket': 2.1.0(@push.rocks/smartserve@1.1.2)
'@push.rocks/smartsocket': 2.1.0(@push.rocks/smartserve@1.3.0)
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
optionalDependencies:
'@push.rocks/smartserve': 1.1.2
'@push.rocks/smartserve': 1.3.0
transitivePeerDependencies:
- '@nuxt/kit'
- bufferutil
@@ -3792,7 +3792,7 @@ snapshots:
- utf-8-validate
- vue
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@1.1.2)':
'@api.global/typedsocket@4.1.0(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedrequest-interfaces': 3.0.19
@@ -3801,7 +3801,7 @@ snapshots:
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smartserve': 1.1.2
'@push.rocks/smartserve': 1.3.0
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0
@@ -4598,9 +4598,9 @@ snapshots:
'@push.rocks/smartshell': 3.3.0
tsx: 4.21.0
'@git.zone/tswatch@2.2.3(@push.rocks/smartserve@1.1.2)':
'@git.zone/tswatch@2.2.3(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.1.2)
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.3.0)
'@git.zone/tsbundle': 2.6.3
'@git.zone/tsrun': 2.0.0
'@push.rocks/early': 4.0.4
@@ -5380,7 +5380,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
rxjs: 7.8.2
'@push.rocks/smartserve@1.1.2':
'@push.rocks/smartserve@1.3.0':
dependencies:
'@api.global/typedrequest': 3.2.5
'@push.rocks/lik': 6.2.2
@@ -5410,10 +5410,10 @@ snapshots:
'@push.rocks/webrequest': 4.0.1
'@tsclass/tsclass': 9.3.0
'@push.rocks/smartsocket@2.1.0(@push.rocks/smartserve@1.1.2)':
'@push.rocks/smartsocket@2.1.0(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.1.2)
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.3.0)
'@push.rocks/isohash': 2.0.1
'@push.rocks/isounique': 1.0.5
'@push.rocks/lik': 6.2.2
@@ -5727,11 +5727,11 @@ snapshots:
'@push.rocks/smartlog-interfaces': 3.0.2
'@tsclass/tsclass': 4.4.4
'@serve.zone/platformclient@1.1.2(@push.rocks/smartserve@1.1.2)':
'@serve.zone/platformclient@1.1.2(@push.rocks/smartserve@1.3.0)':
dependencies:
'@api.global/typedrequest': 3.2.5
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.1.2)
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.1.2)
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@1.3.0)
'@api.global/typedsocket': 3.1.1(@push.rocks/smartserve@1.3.0)
'@push.rocks/qenv': 6.1.3
'@push.rocks/smartlog': 3.1.10
'@push.rocks/smartntml': 2.0.8
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@idp.global/idp.global',
version: '1.9.0',
version: '1.10.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.'
}
+11
View File
@@ -8,6 +8,17 @@ export const runCli = async () => {
feedMetadata: null,
domain: 'idp.global',
serveDir: paths.distWebDir,
securityHeaders: {
csp: {
defaultSrc: "'self'",
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.paddle.com", "https://public.profitwell.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.paddle.com", "https://assetbroker.lossless.one"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "data:"],
connectSrc: ["'self'", "https://*.paddle.com", "https://buy.paddle.com", "https://checkout.paddle.com", "https://checkout-service.paddle.com", "https://cdn.paddle.com", "https://*.sentry.io", "https://public.profitwell.com", "wss:"],
frameSrc: ["https://buy.paddle.com", "https://checkout.paddle.com", "https://*.paddle.com"],
},
},
addCustomRoutes: async (typedserver) => {
// Enable SPA fallback - serves index.html for non-file routes (e.g., /login, /dashboard)
typedserver.options.spaFallback = true;
+12 -1
View File
@@ -59,6 +59,17 @@ export class BillingPlanManager {
}
}
}
}))
}));
// Paddle configuration endpoint
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_GetPaddleConfig>(
'getPaddleConfig',
async () => ({
paddleToken: await this.receptionRef.serviceQenv.getEnvVarOnDemand('PADDLE_TOKEN'),
paddlePriceId: await this.receptionRef.serviceQenv.getEnvVarOnDemand('PADDLE_PRICE_ID'),
})
)
);
}
}
@@ -37,3 +37,19 @@ export interface IReq_GetBillingPlan
billingPlan: data.IBillingPlan;
};
}
/**
* Returns Paddle configuration from environment variables
*/
export interface IReq_GetPaddleConfig
extends plugins.typedRequestInterfaces.implementsTR<
plugins.typedRequestInterfaces.ITypedRequest,
IReq_GetPaddleConfig
> {
method: 'getPaddleConfig';
request: {};
response: {
paddleToken: string;
paddlePriceId: string;
};
}
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@idp.global/idp.global',
version: '1.9.0',
version: '1.10.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.'
}
+32 -9
View File
@@ -11,6 +11,7 @@ import {
import * as plugins from '../../../plugins.js';
import sharedStyles from '../sharedstyles.js';
import * as state from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
declare global {
interface HTMLElementTagNameMap {
@@ -61,28 +62,50 @@ export class PaddleSetupView extends DeesElement {
public async openPaddle() {
await this.domtoolsPromise;
const paddleButton = this.shadowRoot.querySelector('dees-button');
await this.domtools.setExternalScript('https://cdn.paddle.com/paddle/paddle.js');
globalThis.Paddle.Setup({
vendor: 30954,
const idpState = await IdpState.getSingletonInstance();
// Get user email - first try from state, then fetch directly
let userEmail = state.accountState.getState().user?.data?.email;
if (!userEmail) {
// State not loaded, fetch user directly
const whoIsResponse = await idpState.idpClient.whoIs().catch(() => null);
userEmail = whoIsResponse?.user?.data?.email;
}
if (!userEmail) {
console.error('Unable to get user email for Paddle checkout');
paddleButton.status = 'error';
paddleButton.text = 'Error: Not logged in';
return;
}
// Fetch Paddle config from backend
const configRequest = idpState.idpClient.typedsocket
.createTypedRequest<plugins.idpInterfaces.request.IReq_GetPaddleConfig>('getPaddleConfig');
const { paddleToken, paddlePriceId } = await configRequest.fire({});
await this.domtools.setExternalScript('https://cdn.paddle.com/paddle/v2/paddle.js');
globalThis.Paddle.Initialize({
token: paddleToken,
eventCallback: async (dataArg: any) => {
// The data.event will specify the event type
if (dataArg.event === 'Checkout.Complete') {
const data: plugins.idpInterfaces.data.IPaddleCheckoutData = dataArg.eventData;
// Paddle Billing v2 event handling
if (dataArg.name === 'checkout.completed') {
const paddleIframe = document.body.querySelector('iframe');
if (paddleIframe) {
document.body.removeChild(paddleIframe);
}
paddleButton.status = 'pending';
paddleButton.text = 'Processing...';
await state.accountState.dispatchAction(state.updatePaddleCheckoutId, data.checkout.id);
await state.accountState.dispatchAction(state.updatePaddleCheckoutId, dataArg.data.transaction_id);
paddleButton.status = 'success';
paddleButton.text = 'Paddle connected!'
}
},
});
globalThis.Paddle.Checkout.open({
product: 561076,
email: 'phil@kunz.io',
items: [{ priceId: paddlePriceId, quantity: 1 }],
customer: { email: userEmail },
});
}
}