logicaffeine_web/ui/pages/
success.rs1use dioxus::prelude::*;
17use crate::ui::router::Route;
18use crate::ui::state::{LicenseState, LicensePlan};
19use crate::ui::components::main_nav::{MainNav, ActivePage};
20use crate::ui::seo::PageHead;
21
22const LICENSE_API_URL: &str = "https://api.logicaffeine.com/session";
23
24const SUCCESS_STYLE: &str = r#"
25.success-container {
26 min-height: 100vh;
27 display: flex;
28 flex-direction: column;
29 align-items: center;
30 justify-content: center;
31 padding: 60px 20px;
32 text-align: center;
33 max-width: 600px;
34 margin: 0 auto;
35}
36
37.success-icon {
38 width: 80px;
39 height: 80px;
40 background: linear-gradient(135deg, #00d4ff 0%, #7b2cbf 100%);
41 border-radius: 50%;
42 display: flex;
43 align-items: center;
44 justify-content: center;
45 margin-bottom: 32px;
46 font-size: 40px;
47}
48
49.success-title {
50 font-size: 36px;
51 font-weight: 700;
52 color: #fff;
53 margin-bottom: 16px;
54}
55
56.success-message {
57 color: #aaa;
58 font-size: 18px;
59 line-height: 1.6;
60 margin-bottom: 32px;
61}
62
63.license-box {
64 background: rgba(0, 212, 255, 0.1);
65 border: 1px solid rgba(0, 212, 255, 0.3);
66 border-radius: 12px;
67 padding: 24px;
68 margin-bottom: 32px;
69 width: 100%;
70 max-width: 400px;
71}
72
73.license-label {
74 color: #00d4ff;
75 font-size: 14px;
76 font-weight: 600;
77 margin-bottom: 12px;
78 text-transform: uppercase;
79 letter-spacing: 1px;
80}
81
82.license-key {
83 background: rgba(0, 0, 0, 0.3);
84 border: 1px solid rgba(255, 255, 255, 0.1);
85 border-radius: 8px;
86 padding: 16px;
87 font-family: monospace;
88 font-size: 14px;
89 color: #fff;
90 word-break: break-all;
91 margin-bottom: 12px;
92}
93
94.copy-btn {
95 background: linear-gradient(135deg, #00d4ff 0%, #7b2cbf 100%);
96 color: white;
97 border: none;
98 padding: 10px 20px;
99 border-radius: 8px;
100 font-size: 14px;
101 font-weight: 600;
102 cursor: pointer;
103 transition: all 0.2s ease;
104}
105
106.copy-btn:hover {
107 transform: translateY(-1px);
108 box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3);
109}
110
111.license-saved {
112 color: #4ade80;
113 font-size: 13px;
114 margin-top: 12px;
115}
116
117.success-actions {
118 display: flex;
119 flex-direction: column;
120 gap: 16px;
121 width: 100%;
122 max-width: 320px;
123}
124
125.btn-primary {
126 display: block;
127 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
128 color: white;
129 padding: 16px 32px;
130 border-radius: 12px;
131 font-size: 16px;
132 font-weight: 600;
133 text-decoration: none;
134 text-align: center;
135 transition: all 0.2s ease;
136}
137
138.btn-primary:hover {
139 transform: translateY(-2px);
140 box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
141}
142
143.btn-secondary {
144 display: block;
145 background: transparent;
146 color: #667eea;
147 padding: 16px 32px;
148 border: 1px solid #667eea;
149 border-radius: 12px;
150 font-size: 16px;
151 font-weight: 500;
152 text-decoration: none;
153 text-align: center;
154 transition: all 0.2s ease;
155}
156
157.btn-secondary:hover {
158 background: rgba(102, 126, 234, 0.1);
159}
160
161.success-note {
162 margin-top: 40px;
163 padding: 20px;
164 background: rgba(255, 255, 255, 0.05);
165 border-radius: 12px;
166 border: 1px solid rgba(255, 255, 255, 0.1);
167}
168
169.success-note p {
170 color: #888;
171 font-size: 14px;
172 margin: 0;
173}
174
175.success-note a {
176 color: #00d4ff;
177 text-decoration: none;
178}
179
180.success-note a:hover {
181 text-decoration: underline;
182}
183
184.loading-spinner {
185 width: 40px;
186 height: 40px;
187 border: 3px solid rgba(0, 212, 255, 0.3);
188 border-top: 3px solid #00d4ff;
189 border-radius: 50%;
190 animation: spin 1s linear infinite;
191 margin: 20px auto;
192}
193
194@keyframes spin {
195 0% { transform: rotate(0deg); }
196 100% { transform: rotate(360deg); }
197}
198
199.error-message {
200 color: #ef4444;
201 background: rgba(239, 68, 68, 0.1);
202 border: 1px solid rgba(239, 68, 68, 0.3);
203 border-radius: 8px;
204 padding: 16px;
205 margin-bottom: 24px;
206}
207"#;
208
209const STRIPE_CUSTOMER_PORTAL: &str = "https://billing.stripe.com/p/login/8x200l3VN98D7qa1SMe3e00";
210
211fn save_license_to_storage(license_key: &str, plan: &str) {
212 if let Some(window) = web_sys::window() {
213 if let Ok(Some(storage)) = window.local_storage() {
214 let _ = storage.set_item("logos_license_key", license_key);
215 let _ = storage.set_item("logos_license_plan", plan);
216 let timestamp = js_sys::Date::now().to_string();
217 let _ = storage.set_item("logos_license_validated_at", ×tamp);
218 }
219 }
220}
221
222fn copy_to_clipboard(text: &str) {
223 if let Some(window) = web_sys::window() {
224 let clipboard = window.navigator().clipboard();
225 let _ = clipboard.write_text(text);
226 }
227}
228
229#[derive(Clone, PartialEq)]
230enum LicenseStatus {
231 Loading,
232 Success { subscription_id: String, plan: String },
233 Error(String),
234 NoSession,
235}
236
237async fn fetch_license_from_session(session_id: String) -> LicenseStatus {
238 use gloo_net::http::Request;
239
240 let body = serde_json::json!({ "sessionId": session_id });
241
242 let response = Request::post(LICENSE_API_URL)
243 .header("Content-Type", "application/json")
244 .body(body.to_string())
245 .unwrap()
246 .send()
247 .await;
248
249 match response {
250 Ok(resp) => {
251 if resp.ok() {
252 match resp.json::<serde_json::Value>().await {
253 Ok(data) => {
254 let subscription_id = data["subscriptionId"]
255 .as_str()
256 .unwrap_or("")
257 .to_string();
258 let plan = data["plan"]
259 .as_str()
260 .unwrap_or("unknown")
261 .to_string();
262 LicenseStatus::Success { subscription_id, plan }
263 }
264 Err(_) => LicenseStatus::Error("Failed to parse response".to_string()),
265 }
266 } else {
267 LicenseStatus::Error("License lookup failed".to_string())
268 }
269 }
270 Err(e) => LicenseStatus::Error(format!("Network error: {}", e)),
271 }
272}
273
274#[component]
275pub fn Success(session_id: Option<String>) -> Element {
276 let mut license_status = use_signal(|| LicenseStatus::Loading);
277 let mut copied = use_signal(|| false);
278 let mut saved = use_signal(|| false);
279 let license_state = use_context::<LicenseState>();
280
281 use_effect(move || {
282 #[cfg(target_arch = "wasm32")]
284 if session_id.is_none() {
285 crate::ui::router::replace_bar_url("/success");
286 }
287
288 let mut license_state = license_state.clone();
289 let session_id = session_id.clone();
290 spawn(async move {
291 if let Some(session_id) = session_id {
292 let result = fetch_license_from_session(session_id).await;
293 if let LicenseStatus::Success { ref subscription_id, ref plan } = result {
294 save_license_to_storage(subscription_id, plan);
295 license_state.set_license(
296 subscription_id.clone(),
297 LicensePlan::from_str(plan),
298 );
299 saved.set(true);
300 }
301 license_status.set(result);
302 } else {
303 license_status.set(LicenseStatus::NoSession);
304 }
305 });
306 });
307
308 let on_copy = move |_| {
309 if let LicenseStatus::Success { ref subscription_id, .. } = *license_status.read() {
310 copy_to_clipboard(subscription_id);
311 copied.set(true);
312 }
313 };
314
315 let (has_license, license_key) = match &*license_status.read() {
316 LicenseStatus::Success { subscription_id, .. } => (true, subscription_id.clone()),
317 _ => (false, String::new()),
318 };
319
320 let is_loading = matches!(*license_status.read(), LicenseStatus::Loading);
321
322 rsx! {
323 PageHead {
324 title: "Payment Success - LOGICAFFEINE",
325 description: "Your payment was successful. License activation and key retrieval.",
326 canonical_path: "/success",
327 }
328 document::Meta { name: "robots", content: "noindex" }
330 style { "{SUCCESS_STYLE}" }
331
332 MainNav { active: ActivePage::Pricing, subtitle: Some("Payment Complete"), show_nav_links: false }
333
334 div { class: "success-container",
335 div { class: "success-icon", "✓" }
336
337 h1 { class: "success-title", "Thank You!" }
338
339 p { class: "success-message",
340 "Your payment was successful. Welcome to logicaffeine! "
341 "You now have access to use LOGOS for commercial purposes."
342 }
343
344 if is_loading {
345 div { class: "loading-spinner" }
346 p { class: "success-message", "Retrieving your license..." }
347 }
348
349 match &*license_status.read() {
350 LicenseStatus::Error(msg) => rsx! {
351 div { class: "error-message", "{msg}" }
352 },
353 LicenseStatus::NoSession => rsx! {
354 div { class: "error-message", "No checkout session found. Please try again." }
355 },
356 _ => rsx! {}
357 }
358
359 if has_license {
360 div { class: "license-box",
361 div { class: "license-label", "Your License Key" }
362 div { class: "license-key", "{license_key}" }
363 button {
364 class: "copy-btn",
365 onclick: on_copy,
366 if *copied.read() { "Copied!" } else { "Copy to Clipboard" }
367 }
368 if *saved.read() {
369 div { class: "license-saved",
370 "✓ License saved to your browser"
371 }
372 }
373 }
374 }
375
376 div { class: "success-actions",
377 Link {
378 class: "btn-primary",
379 to: Route::Studio { file: None },
380 "Open Studio"
381 }
382
383 a {
384 class: "btn-secondary",
385 href: STRIPE_CUSTOMER_PORTAL,
386 target: "_blank",
387 "Manage Subscription"
388 }
389 }
390
391 div { class: "success-note",
392 p {
393 "Save your license key somewhere safe. "
394 "Need help? Contact us at "
395 a { href: "mailto:[email protected]", "[email protected]" }
396 "."
397 }
398 }
399 }
400 }
401}