rust_x402/template/
mod.rs

1//! HTML template system for x402 paywall
2//!
3//! This module provides HTML template generation for the x402 paywall,
4//! similar to the Python implementation but using Rust's type system.
5
6pub mod config;
7pub mod paywall;
8
9use crate::types::PaymentRequirements;
10use serde_json;
11
12/// Template configuration for paywall customization
13#[derive(Debug, Clone, Default)]
14pub struct PaywallConfig {
15    /// App name displayed in the paywall
16    pub app_name: Option<String>,
17    /// App logo URL
18    pub app_logo: Option<String>,
19    /// CDP client key for enhanced RPC
20    pub cdp_client_key: Option<String>,
21    /// Session token endpoint
22    pub session_token_endpoint: Option<String>,
23    /// Custom CSS styles
24    pub custom_css: Option<String>,
25    /// Custom JavaScript
26    pub custom_js: Option<String>,
27    /// Theme configuration
28    pub theme: Option<ThemeConfig>,
29    /// Branding configuration
30    pub branding: Option<BrandingConfig>,
31}
32
33/// Theme configuration for the paywall
34#[derive(Debug, Clone)]
35pub struct ThemeConfig {
36    /// Primary color
37    pub primary_color: String,
38    /// Secondary color
39    pub secondary_color: String,
40    /// Background color
41    pub background_color: String,
42    /// Text color
43    pub text_color: String,
44    /// Border radius
45    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/// Branding configuration for the paywall
61#[derive(Debug, Clone)]
62pub struct BrandingConfig {
63    /// Company name
64    pub company_name: String,
65    /// Company logo URL
66    pub company_logo: Option<String>,
67    /// Support email
68    pub support_email: Option<String>,
69    /// Support URL
70    pub support_url: Option<String>,
71    /// Terms of service URL
72    pub terms_url: Option<String>,
73    /// Privacy policy URL
74    pub privacy_url: Option<String>,
75}
76
77impl PaywallConfig {
78    /// Create a new paywall config
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Set the app name
84    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    /// Set the app logo
90    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    /// Set the CDP client key
96    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    /// Set the session token endpoint
102    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    /// Set custom CSS
108    pub fn with_custom_css(mut self, css: impl Into<String>) -> Self {
109        self.custom_css = Some(css.into());
110        self
111    }
112
113    /// Set custom JavaScript
114    pub fn with_custom_js(mut self, js: impl Into<String>) -> Self {
115        self.custom_js = Some(js.into());
116        self
117    }
118
119    /// Set theme configuration
120    pub fn with_theme(mut self, theme: ThemeConfig) -> Self {
121        self.theme = Some(theme);
122        self
123    }
124
125    /// Set branding configuration
126    pub fn with_branding(mut self, branding: BrandingConfig) -> Self {
127        self.branding = Some(branding);
128        self
129    }
130}
131
132impl ThemeConfig {
133    /// Create a new theme config
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Set primary color
139    pub fn with_primary_color(mut self, color: impl Into<String>) -> Self {
140        self.primary_color = color.into();
141        self
142    }
143
144    /// Set secondary color
145    pub fn with_secondary_color(mut self, color: impl Into<String>) -> Self {
146        self.secondary_color = color.into();
147        self
148    }
149
150    /// Set background color
151    pub fn with_background_color(mut self, color: impl Into<String>) -> Self {
152        self.background_color = color.into();
153        self
154    }
155
156    /// Set text color
157    pub fn with_text_color(mut self, color: impl Into<String>) -> Self {
158        self.text_color = color.into();
159        self
160    }
161
162    /// Set border radius
163    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    /// Create a new branding config
171    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    /// Set company logo
183    pub fn with_company_logo(mut self, logo: impl Into<String>) -> Self {
184        self.company_logo = Some(logo.into());
185        self
186    }
187
188    /// Set support email
189    pub fn with_support_email(mut self, email: impl Into<String>) -> Self {
190        self.support_email = Some(email.into());
191        self
192    }
193
194    /// Set support URL
195    pub fn with_support_url(mut self, url: impl Into<String>) -> Self {
196        self.support_url = Some(url.into());
197        self
198    }
199
200    /// Set terms of service URL
201    pub fn with_terms_url(mut self, url: impl Into<String>) -> Self {
202        self.terms_url = Some(url.into());
203        self
204    }
205
206    /// Set privacy policy URL
207    pub fn with_privacy_url(mut self, url: impl Into<String>) -> Self {
208        self.privacy_url = Some(url.into());
209        self
210    }
211}
212
213/// Generate paywall HTML with injected configuration
214pub 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
223/// Inject payment data into HTML template
224fn 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    // Create the configuration script
234    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    // Apply theme customizations if provided
243    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    // Inject the configuration script into the head
263    html.replace("</head>", &format!("{}\n</head>", config_script))
264}
265
266/// Apply theme customizations to HTML
267fn 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
287/// Apply branding customizations to HTML
288fn apply_branding_customizations(html: &str, branding: &BrandingConfig) -> String {
289    let mut html = html.to_string();
290
291    // Replace app name in title
292    html = html.replace(
293        "Payment Required",
294        &format!("{} - Payment Required", branding.company_name),
295    );
296
297    // Replace logo if provided
298    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
312/// Inject custom CSS into HTML
313fn 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
318/// Inject custom JavaScript into HTML
319fn 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
324/// Create x402 configuration object from payment requirements
325fn 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        // Convert atomic amount back to USD (assuming USDC with 6 decimals)
337        if let Ok(amount) = req.max_amount_required.parse::<f64>() {
338            display_amount = amount / 1_000_000.0; // USDC has 6 decimals
339        }
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    // Add theme configuration if provided
361    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    // Add branding configuration if provided
372    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
386/// Check if request is from a browser
387pub fn is_browser_request(user_agent: &str, accept: &str) -> bool {
388    accept.contains("text/html") && user_agent.contains("Mozilla")
389}