1pub mod config;
7pub mod paywall;
8
9use crate::types::PaymentRequirements;
10use serde_json;
11
12#[derive(Debug, Clone, Default)]
14pub struct PaywallConfig {
15 pub app_name: Option<String>,
17 pub app_logo: Option<String>,
19 pub cdp_client_key: Option<String>,
21 pub session_token_endpoint: Option<String>,
23 pub custom_css: Option<String>,
25 pub custom_js: Option<String>,
27 pub theme: Option<ThemeConfig>,
29 pub branding: Option<BrandingConfig>,
31}
32
33#[derive(Debug, Clone)]
35pub struct ThemeConfig {
36 pub primary_color: String,
38 pub secondary_color: String,
40 pub background_color: String,
42 pub text_color: String,
44 pub border_radius: String,
46}
47
48impl Default for ThemeConfig {
49 fn default() -> Self {
50 Self {
51 primary_color: "#667eea".to_string(),
52 secondary_color: "#764ba2".to_string(),
53 background_color: "#ffffff".to_string(),
54 text_color: "#1a1a1a".to_string(),
55 border_radius: "16px".to_string(),
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct BrandingConfig {
63 pub company_name: String,
65 pub company_logo: Option<String>,
67 pub support_email: Option<String>,
69 pub support_url: Option<String>,
71 pub terms_url: Option<String>,
73 pub privacy_url: Option<String>,
75}
76
77impl PaywallConfig {
78 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn with_app_name(mut self, app_name: impl Into<String>) -> Self {
85 self.app_name = Some(app_name.into());
86 self
87 }
88
89 pub fn with_app_logo(mut self, app_logo: impl Into<String>) -> Self {
91 self.app_logo = Some(app_logo.into());
92 self
93 }
94
95 pub fn with_cdp_client_key(mut self, cdp_client_key: impl Into<String>) -> Self {
97 self.cdp_client_key = Some(cdp_client_key.into());
98 self
99 }
100
101 pub fn with_session_token_endpoint(mut self, endpoint: impl Into<String>) -> Self {
103 self.session_token_endpoint = Some(endpoint.into());
104 self
105 }
106
107 pub fn with_custom_css(mut self, css: impl Into<String>) -> Self {
109 self.custom_css = Some(css.into());
110 self
111 }
112
113 pub fn with_custom_js(mut self, js: impl Into<String>) -> Self {
115 self.custom_js = Some(js.into());
116 self
117 }
118
119 pub fn with_theme(mut self, theme: ThemeConfig) -> Self {
121 self.theme = Some(theme);
122 self
123 }
124
125 pub fn with_branding(mut self, branding: BrandingConfig) -> Self {
127 self.branding = Some(branding);
128 self
129 }
130}
131
132impl ThemeConfig {
133 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn with_primary_color(mut self, color: impl Into<String>) -> Self {
140 self.primary_color = color.into();
141 self
142 }
143
144 pub fn with_secondary_color(mut self, color: impl Into<String>) -> Self {
146 self.secondary_color = color.into();
147 self
148 }
149
150 pub fn with_background_color(mut self, color: impl Into<String>) -> Self {
152 self.background_color = color.into();
153 self
154 }
155
156 pub fn with_text_color(mut self, color: impl Into<String>) -> Self {
158 self.text_color = color.into();
159 self
160 }
161
162 pub fn with_border_radius(mut self, radius: impl Into<String>) -> Self {
164 self.border_radius = radius.into();
165 self
166 }
167}
168
169impl BrandingConfig {
170 pub fn new(company_name: impl Into<String>) -> Self {
172 Self {
173 company_name: company_name.into(),
174 company_logo: None,
175 support_email: None,
176 support_url: None,
177 terms_url: None,
178 privacy_url: None,
179 }
180 }
181
182 pub fn with_company_logo(mut self, logo: impl Into<String>) -> Self {
184 self.company_logo = Some(logo.into());
185 self
186 }
187
188 pub fn with_support_email(mut self, email: impl Into<String>) -> Self {
190 self.support_email = Some(email.into());
191 self
192 }
193
194 pub fn with_support_url(mut self, url: impl Into<String>) -> Self {
196 self.support_url = Some(url.into());
197 self
198 }
199
200 pub fn with_terms_url(mut self, url: impl Into<String>) -> Self {
202 self.terms_url = Some(url.into());
203 self
204 }
205
206 pub fn with_privacy_url(mut self, url: impl Into<String>) -> Self {
208 self.privacy_url = Some(url.into());
209 self
210 }
211}
212
213pub fn generate_paywall_html(
215 error: &str,
216 payment_requirements: &[PaymentRequirements],
217 paywall_config: Option<&PaywallConfig>,
218) -> String {
219 let base_template = paywall::get_base_template();
220 inject_payment_data(base_template, error, payment_requirements, paywall_config)
221}
222
223fn inject_payment_data(
225 html_content: &str,
226 error: &str,
227 payment_requirements: &[PaymentRequirements],
228 paywall_config: Option<&PaywallConfig>,
229) -> String {
230 let x402_config = create_x402_config(error, payment_requirements, paywall_config);
231 let config_json = serde_json::to_string(&x402_config).unwrap_or_else(|_| "{}".to_string());
232
233 let config_script = format!(
235 r#" <script>
236 window.x402 = {};
237 console.log('Payment requirements initialized:', window.x402);
238 </script>"#,
239 config_json
240 );
241
242 let mut html = html_content.to_string();
244 if let Some(config) = paywall_config {
245 if let Some(theme) = &config.theme {
246 html = apply_theme_customizations(&html, theme);
247 }
248
249 if let Some(branding) = &config.branding {
250 html = apply_branding_customizations(&html, branding);
251 }
252
253 if let Some(custom_css) = &config.custom_css {
254 html = inject_custom_css(&html, custom_css);
255 }
256
257 if let Some(custom_js) = &config.custom_js {
258 html = inject_custom_js(&html, custom_js);
259 }
260 }
261
262 html.replace("</head>", &format!("{}\n</head>", config_script))
264}
265
266fn apply_theme_customizations(html: &str, theme: &ThemeConfig) -> String {
268 let css_vars = format!(
269 r#"
270 :root {{
271 --primary-color: {};
272 --secondary-color: {};
273 --background-color: {};
274 --text-color: {};
275 --border-radius: {};
276 }}"#,
277 theme.primary_color,
278 theme.secondary_color,
279 theme.background_color,
280 theme.text_color,
281 theme.border_radius
282 );
283
284 html.replace("</head>", &format!("{}\n</head>", css_vars))
285}
286
287fn apply_branding_customizations(html: &str, branding: &BrandingConfig) -> String {
289 let mut html = html.to_string();
290
291 html = html.replace(
293 "Payment Required",
294 &format!("{} - Payment Required", branding.company_name),
295 );
296
297 if let Some(logo_url) = &branding.company_logo {
299 let logo_html = format!(
300 r#"<img src="{}" alt="{}" style="width: 80px; height: 80px; object-fit: contain;">"#,
301 logo_url, branding.company_name
302 );
303 html = html.replace(
304 r#"<div class="logo">💰</div>"#,
305 &format!(r#"<div class="logo">{}</div>"#, logo_html),
306 );
307 }
308
309 html
310}
311
312fn inject_custom_css(html: &str, css: &str) -> String {
314 let css_tag = format!(r#"<style>{}</style>"#, css);
315 html.replace("</head>", &format!("{}\n</head>", css_tag))
316}
317
318fn inject_custom_js(html: &str, js: &str) -> String {
320 let js_tag = format!(r#"<script>{}</script>"#, js);
321 html.replace("</body>", &format!("{}\n</body>", js_tag))
322}
323
324fn create_x402_config(
326 error: &str,
327 payment_requirements: &[PaymentRequirements],
328 paywall_config: Option<&PaywallConfig>,
329) -> serde_json::Value {
330 let requirements = payment_requirements.first();
331 let mut display_amount = 0.0;
332 let mut current_url = String::new();
333 let mut testnet = true;
334
335 if let Some(req) = requirements {
336 if let Ok(amount) = req.max_amount_required.parse::<f64>() {
338 display_amount = amount / 1_000_000.0; }
340 current_url = req.resource.clone();
341 testnet = req.network == "base-sepolia";
342 }
343
344 let default_config = PaywallConfig::default();
345 let config = paywall_config.unwrap_or(&default_config);
346
347 let mut config_json = serde_json::json!({
348 "amount": display_amount,
349 "paymentRequirements": payment_requirements,
350 "testnet": testnet,
351 "currentUrl": current_url,
352 "error": error,
353 "x402_version": 1,
354 "cdpClientKey": config.cdp_client_key.as_deref().unwrap_or(""),
355 "appName": config.app_name.as_deref().unwrap_or(""),
356 "appLogo": config.app_logo.as_deref().unwrap_or(""),
357 "sessionTokenEndpoint": config.session_token_endpoint.as_deref().unwrap_or(""),
358 });
359
360 if let Some(theme) = &config.theme {
362 config_json["theme"] = serde_json::json!({
363 "primaryColor": theme.primary_color,
364 "secondaryColor": theme.secondary_color,
365 "backgroundColor": theme.background_color,
366 "textColor": theme.text_color,
367 "borderRadius": theme.border_radius,
368 });
369 }
370
371 if let Some(branding) = &config.branding {
373 config_json["branding"] = serde_json::json!({
374 "companyName": branding.company_name,
375 "companyLogo": branding.company_logo,
376 "supportEmail": branding.support_email,
377 "supportUrl": branding.support_url,
378 "termsUrl": branding.terms_url,
379 "privacyUrl": branding.privacy_url,
380 });
381 }
382
383 config_json
384}
385
386pub fn is_browser_request(user_agent: &str, accept: &str) -> bool {
388 accept.contains("text/html") && user_agent.contains("Mozilla")
389}