diff --git a/Modern Development/Service Portal Widgets/Stripe Payment Widget/Readme.md b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Readme.md new file mode 100644 index 0000000000..c5ec24d534 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Readme.md @@ -0,0 +1,91 @@ +# πŸ’³ Stripe Payment Widget for ServiceNow Service Portal + +### Overview + +The **Stripe Payment Widget** enables secure and modern payment experiences directly inside the **ServiceNow Service Portal** using **Stripe.js v3**. +It supports one-time payments and subscription flows, with integrated address fields, real-time validation, and full Stripe Element styling. + +Built entirely using **client**, **server**, and **HTML/CSS** components β€” this widget can be plugged into any portal page to accept payments or donations seamlessly. + +--- + +### ✨ Features + +- πŸ” Powered by **Stripe.js v3** (secure, PCI-compliant) +- 🧾 Supports **PaymentIntent** and **Subscription** flows +- 🌍 Includes **Address Element** and live validation +- πŸ’¬ Displays dynamic messages and status indicators +- πŸ’… Modern, responsive design using Tailwind-like CSS +- βš™οΈ Fully compatible with ServiceNow **Service Portal widgets** +- πŸ”’ No card details ever stored in ServiceNow + +--- + +### 🧩 Folder Structure + +| File Name | Description | +| ---------------------------- | -------------------------------------------------------------------------------------------- | +| `Readme.md` | Project documentation | +| `Stripe_Payment_html` | Widget HTML template | +| `Stripe_Payment_css` | Widget styling (responsive modern design) | +| `Stripe_Payment_client_code` | AngularJS client controller with Stripe.js logic | +| `Stripe_Payment_server_code` | Server script handling secure backend processing via `StripePaymentProcessor` Script Include | + +--- + +### 🧰 Technical Dependencies + +- **ServiceNow Instance** with Service Portal enabled +- **Stripe API Key** stored in a system property: + - Property Name β†’ `stripe_element_key` + - Example Value β†’ `pk_test_51Nxxxxx` +- **Script Include**: `StripePaymentProcessor` + Handles backend communication with Stripe API securely. + +--- + +### βš™οΈ Installation Steps + +1. Create a **new Service Portal Widget** in your instance. +2. Copy each file’s content into its respective section: + - HTML β†’ Widget HTML + - CSS β†’ Widget CSS + - Client Script β†’ Widget Client Controller + - Server Script β†’ Widget Server Script +3. Add a system property: + - Name: `stripe_element_key` + - Value: Your Stripe Publishable Key +4. Add the widget to a portal page (like `/checkout` or `/donate`). +5. (Optional) Create Script Include: `StripePaymentProcessor` to handle backend API calls. + +--- + +### πŸ’³ Example Use Cases + +- πŸŽ—οΈ Donation collection for NGOs / non-profits +- 🧾 Subscription renewal systems +- πŸ›’ Checkout page for internal app store +- πŸ’Ό Invoice or expense reimbursement payment portals + +--- + +### 🧠 Future Enhancements + +- Support **Apple Pay** and **Google Pay** +- Add **multi-currency** and **tax calculation** +- Enable **webhook-based status sync** +- Dashboard to track transactions and refunds + +--- + +### πŸ§‘β€πŸ’» Author + +**Abhishek Aggarwal** +ServiceNow Developer | Hacktoberfest 2025 Contributor +🌐 [GitHub Profile](https://github.com/abhishek-aggarwal) + +--- + +### 🏷️ Tags + +`ServiceNow` `ServicePortal` `Stripe` `Hacktoberfest` `Widget` `Payment Integration` `Donation` diff --git a/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_client_code.js b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_client_code.js new file mode 100644 index 0000000000..38d2339d93 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_client_code.js @@ -0,0 +1,970 @@ +api.controller = function ( + $scope, + $window, + $rootScope, + $timeout, + $http, + spUtil +) { + var c = this; + + // Widget state initialization + c.stripe = null; + c.elements = null; + c.cardNumber = null; + c.cardExpiry = null; + c.cardCvc = null; + c.addressElement = null; + c.addressData = null; + c.isProcessing = false; + c.errorMessage = ""; + c.successMessage = ""; + c.processingMessage = ""; + c.fieldErrors = { + cardNumber: "", + cardExpiry: "", + cardCvc: "", + address: "", + }; + c.sessionToken = null; + c.inactivityTimeout = null; + c.paymentTimeout = null; + c.paymentCompleted = false; + + // Constants + var INACTIVITY_TIMEOUT = 8 * 60 * 1000; // 8 minutes in milliseconds + var MAIN_PAGE_URL = "?id=home_page_npsm"; + var PAYMENT_SUCCESS_PAGE = "?id=payment_success_page"; + + // Utility function for safe Angular apply + function safeApply(fn) { + var phase = $scope.$root.$$phase; + if (phase !== "$apply" && phase !== "$digest") { + $scope.$apply(fn); + } else if (fn && typeof fn === "function") { + fn(); + } + } + + // Structured logging service (replace with your logging mechanism) + function logMessage(level, message, data) { + if ($window.spUtil && spUtil.addInfoMessage) { + spUtil.addInfoMessage( + `[${level}] ${message}: ${JSON.stringify(data || {})}` + ); + } + } + + // Initialize session token for refresh handling + function initializeSession() { + c.sessionToken = $window.sessionStorage.getItem("paymentSessionToken"); + if (!c.sessionToken) { + c.sessionToken = + "sess_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); + $window.sessionStorage.setItem("paymentSessionToken", c.sessionToken); + var params = new URLSearchParams(); + params.set("session", sessionStorage.getItem("paymentSessionToken")); + } + } + + // Start inactivity and payment timers + function startTimers() { + resetInactivityTimer(); + startPaymentTimer(); + + // Reset inactivity timer on user activity + ["mousemove", "keydown", "touchstart", "scroll"].forEach(function (event) { + $window.addEventListener(event, resetInactivityTimer, { + passive: true, + }); + }); + } + + // Reset inactivity timer + function resetInactivityTimer() { + if (c.inactivityTimeout) { + $timeout.cancel(c.inactivityTimeout); + } + c.inactivityTimeout = $timeout(function () { + endSession("Inactivity timeout after 8 minutes"); + }, INACTIVITY_TIMEOUT); + } + + // Start payment timer (8 minutes) + function startPaymentTimer() { + c.paymentTimeout = $timeout(function () { + if (!c.paymentCompleted) { + endSession("Payment not completed within 8 minutes"); + } + }, INACTIVITY_TIMEOUT); + } + + // End session and redirect + function endSession(reason) { + logMessage("INFO", "Ending session", { + reason: reason, + }); + cleanup(); + $window.sessionStorage.removeItem("paymentSessionToken"); + $window.location.href = MAIN_PAGE_URL; + } + + // Initialize Stripe + c.initializeStripe = function () { + return new Promise(function (resolve, reject) { + if ($window.Stripe) { + resolve(); + return; + } + var script = $window.document.createElement("script"); + script.src = "https://js.stripe.com/v3/"; + script.async = true; + script.onload = function () { + logMessage("INFO", "Stripe.js loaded successfully"); + resolve(); + }; + script.onerror = function () { + logMessage("ERROR", "Failed to load Stripe.js"); + reject(new Error("Failed to load Stripe.js")); + }; + $window.document.head.appendChild(script); + }); + }; + + // Initialize Stripe Elements + c.initializeStripeElements = function () { + try { + var stripeKey = c.data.stripeKey; + c.stripe = $window.Stripe(stripeKey); + + var appearance = { + theme: "stripe", + variables: { + colorPrimary: "#667eea", + colorBackground: "#ffffff", + colorText: "#374151", + colorDanger: "#ef4444", + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + spacingUnit: "4px", + borderRadius: "6px", + }, + rules: { + ".Input": { + border: "2px solid #e5e7eb", + padding: "12px", + fontSize: "14px", + }, + ".Input:focus": { + borderColor: "#667eea", + boxShadow: "0 0 0 3px rgba(102, 126, 234, 0.1)", + }, + }, + }; + + c.elements = c.stripe.elements({ + appearance: appearance, + }); + c.initializeCardElements(); + c.initializeAddressElement(); + } catch (error) { + logMessage("ERROR", "Error initializing Stripe elements", { + error: error.message, + }); + safeApply(function () { + c.errorMessage = + "Error initializing payment form. Please try again later."; + c.showError(c.errorMessage); + }); + } + }; + + // Initialize Card Elements + c.initializeCardElements = function () { + var cardStyle = { + base: { + color: "#374151", + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + fontSmoothing: "antialiased", + fontSize: "16px", + lineHeight: "24px", + "::placeholder": { + color: "#9ca3af", + fontSize: "14px", + }, + }, + invalid: { + color: "#ef4444", + iconColor: "#ef4444", + }, + complete: { + color: "#10b981", + iconColor: "#10b981", + }, + }; + + c.cardNumber = c.elements.create("cardNumber", { + style: cardStyle, + showIcon: true, + }); + c.cardExpiry = c.elements.create("cardExpiry", { + style: cardStyle, + }); + c.cardCvc = c.elements.create("cardCvc", { + style: cardStyle, + }); + + $timeout(function () { + c.mountCardElements(); + }, 300); + }; + + // Mount Card Elements + c.mountCardElements = function () { + try { + var elements = [ + { + element: c.cardNumber, + id: "stripe-card-number", + field: "cardNumber", + }, + { + element: c.cardExpiry, + id: "stripe-card-expiry", + field: "cardExpiry", + }, + { + element: c.cardCvc, + id: "stripe-card-cvc", + field: "cardCvc", + }, + ]; + + elements.forEach(function (item) { + var container = $window.document.getElementById(item.id); + if (container) { + item.element.mount("#" + item.id); + c.setupCardElementEvents(item.element, item.field, item.id); + } else { + logMessage("ERROR", "Element container not found", { + id: item.id, + }); + throw new Error("Element container not found: " + item.id); + } + }); + + c.attachFormHandler(); + } catch (error) { + logMessage("ERROR", "Error mounting card elements", { + error: error.message, + }); + safeApply(function () { + c.errorMessage = "Payment form not available. Please refresh the page."; + c.showError(c.errorMessage); + }); + } + }; + + // Setup Card Element Event Listeners + c.setupCardElementEvents = function (element, fieldName, elementId) { + element.on("change", function (event) { + safeApply(function () { + c.fieldErrors[fieldName] = event.error ? event.error.message : ""; + c.displayFieldError(fieldName, c.fieldErrors[fieldName]); + if ( + !Object.values(c.fieldErrors).some(function (error) { + return error; + }) + ) { + c.errorMessage = ""; + c.clearError(); + } + }); + }); + + element.on("focus", function () { + var domElement = $window.document.getElementById(elementId); + if (domElement) { + domElement.classList.add("focused"); + if (domElement.parentElement) { + domElement.parentElement.classList.add("field-focused"); + } + } + }); + + element.on("blur", function () { + var domElement = $window.document.getElementById(elementId); + if (domElement) { + domElement.classList.remove("focused"); + if (domElement.parentElement) { + domElement.parentElement.classList.remove("field-focused"); + } + } + }); + }; + + // Initialize Address Element + c.initializeAddressElement = function () { + var addressOptions = { + mode: "billing", + autocomplete: { + mode: "automatic", + }, + fields: { + phone: "always", + }, + defaultValues: + c.data && c.data.address + ? { + name: c.data.customer_name || c.data.customerName || "", + address: { + line1: c.data.address.line1 || "", + line2: c.data.address.line2 || "", + city: c.data.address.city || "", + state: c.data.address.state || "", + country: c.data.address.country || "US", + postal_code: c.data.address.postal_code || "", + }, + phone: c.data.customer_phone || c.data.customerPhone || "", + } + : {}, + }; + + c.addressElement = c.elements.create("address", addressOptions); + document.getElementById("email").value = c.data.user_email || ""; + document.getElementById("cardholder-name").value = c.data.user_name; + $timeout(function () { + var container = $window.document.getElementById("address-element"); + if (container) { + c.addressElement.mount("#address-element"); + c.setupAddressEventListeners(); + } else { + logMessage("ERROR", "Address element container not found"); + c.errorMessage = "Address form not available. Please refresh the page."; + c.showError(c.errorMessage); + } + }, 400); + }; + + // Setup Address Event Listeners + c.setupAddressEventListeners = function () { + c.addressElement.on("change", function (event) { + safeApply(function () { + c.fieldErrors.address = event.error ? event.error.message : ""; + c.displayFieldError("address", c.fieldErrors.address); + if (event.complete && event.value) { + c.addressData = event.value; + } + }); + }); + }; + + // Attach Form Handler + c.attachFormHandler = function () { + var form = $window.document.getElementById("payment-form"); + if (form) { + form.removeEventListener("submit", c.handleSubmit); + form.addEventListener("submit", c.handleSubmit); + } else { + logMessage("ERROR", "Payment form not found"); + c.errorMessage = "Payment form not found. Please refresh the page."; + c.showError(c.errorMessage); + } + }; + + // Handle Form Submission + c.handleSubmit = function (event) { + if (event) { + event.preventDefault(); + } + + if (c.isProcessing) { + return; + } + + if (!$window.confirm("Are you sure you want to submit the payment?")) { + return; + } + + safeApply(function () { + if (!c.isFormValid()) { + c.errorMessage = "Please complete all required fields."; + c.showError(c.errorMessage); + return; + } + + if (c.hasFieldErrors()) { + c.errorMessage = "Please correct the errors in the form."; + c.showError(c.errorMessage); + return; + } + + c.isProcessing = true; + c.updateButtonState(true); + c.createPaymentMethod(); + }); + }; + + // Update Button State + c.updateButtonState = function (isLoading) { + var buttonText = $window.document.getElementById("button-text"); + var loadingSpinner = $window.document.getElementById("loading-spinner"); + var submitBtn = $window.document.getElementById("submit-btn"); + + if (isLoading) { + if (buttonText) buttonText.style.display = "none"; + if (loadingSpinner) loadingSpinner.style.display = "inline-block"; + if (submitBtn) submitBtn.disabled = true; + } else { + if (buttonText) buttonText.style.display = "inline"; + if (loadingSpinner) loadingSpinner.style.display = "none"; + if (submitBtn) submitBtn.disabled = false; + } + }; + + // Create Payment Method + c.createPaymentMethod = function () { + if ( + !c.stripe || + !c.cardNumber || + !c.cardExpiry || + !c.cardCvc || + !c.addressData + ) { + c.errorMessage = "Payment form not properly initialized."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + return; + } + + c.processingMessage = "Creating payment method..."; + + var cardholderName = + $window.document.getElementById("cardholder-name")?.value?.trim() || + c.addressData.name || + c.data.customer_name || + "Customer"; + var billingDetails = { + name: cardholderName, + email: c.data.customer_email || null, + address: { + line1: c.addressData.address.line1 || null, + line2: c.addressData.address.line2 || null, + city: c.addressData.address.city || null, + state: c.addressData.address.state || null, + postal_code: c.addressData.address.postal_code || null, + country: c.addressData.address.country || "US", + }, + }; + + c.stripe + .createPaymentMethod({ + type: "card", + card: c.cardNumber, + billing_details: billingDetails, + }) + .then(function (result) { + safeApply(function () { + if (result.error) { + c.errorMessage = + result.error.message || "Failed to create payment method."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + } else { + c.createSubscription(result.paymentMethod); + } + }); + }) + .catch(function (error) { + safeApply(function () { + c.errorMessage = "Error creating payment method. Please try again."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Create Subscription + c.createSubscription = function (paymentMethod) { + c.processingMessage = "Creating subscription..."; + + var subscriptionData = { + payment_method_id: paymentMethod.id, + recordid: c.data.recordid, + customer_info: { + id: c.data.customer_stripeid, + name: c.addressData.name || c.data.customer_name || "Customer", + email: c.data.customer_email || null, + }, + billing_address: c.addressData.address, + subscription_details: { + price_id: c.data.priceId || null, + plan_name: c.data.planName || "Monthly Plan", + trial_period_days: c.data.trialDays || 0, + }, + metadata: { + order_id: c.data.orderId || null, + description: c.data.description || "ServiceNow Subscription", + session_token: c.sessionToken, + }, + }; + + c.server + .get({ + action: "createSubscription", + subscriptionData: subscriptionData, + }) + .then(function (response) { + safeApply(function () { + var responseData = response?.data?.response?.data; + if (responseData?.success) { + c.subscriptionId = responseData.subscription_id; + if ( + responseData.requires_action && + responseData.payment_intent?.id + ) { + c.handle3DSecure(responseData.payment_intent); + } else { + c.checkFinalPaymentConfirmation( + responseData.subscription_id, + responseData.payment_intent?.id + ); + } + } else { + c.errorMessage = + responseData?.error || "Subscription creation failed."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + } + }); + }) + .catch(function (error) { + safeApply(function () { + c.errorMessage = "Server error during subscription creation."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Handle 3D Secure Authentication + c.handle3DSecure = function (paymentIntent) { + c.processingMessage = "Authenticating payment..."; + + c.stripe + .confirmCardPayment(paymentIntent.client_secret, { + payment_method: paymentIntent.payment_method, + }) + .then(function (result) { + safeApply(function () { + if (result.error) { + c.errorMessage = + result.error.message || "Payment authentication failed."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + } else if ( + ["succeeded", "processing"].includes(result.paymentIntent.status) + ) { + c.checkFinalPaymentConfirmation( + c.subscriptionId, + result.paymentIntent.id + ); + } else { + c.errorMessage = "Payment could not be confirmed."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + } + }); + }) + .catch(function (error) { + safeApply(function () { + c.errorMessage = "Error during payment authentication."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + c.subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Check Final Payment Confirmation + c.checkFinalPaymentConfirmation = function (subscriptionId, paymentIntentId) { + c.processingMessage = "Finalizing payment..."; + + c.server + .get({ + action: "checkFinalPaymentConfirmation", + subscription_id: subscriptionId, + payment_intent_id: paymentIntentId, + recordid: c.data.recordid, + }) + .then(function (response) { + safeApply(function () { + var result = response?.data?.response?.data; + if (result?.success && result.payment_confirmed) { + c.paymentCompleted = true; + c.successMessage = + result.message || "Subscription created successfully!"; + c.showSuccess(c.successMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.clearForm(); + c.redirectWithStatus( + "MonkeyLampEchoFuzz9RainyTaxiGlueXenonWaltz32GhostPieClapMint", + subscriptionId, + "", + c.recordid + ); + } else { + c.errorMessage = result?.error || "Payment confirmation failed."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + subscriptionId, + c.errorMessage, + c.recordid + ); + } + }); + }) + .catch(function (error) { + safeApply(function () { + c.errorMessage = "Error confirming payment."; + c.showError(c.errorMessage); + c.isProcessing = false; + c.updateButtonState(false); + c.redirectWithStatus( + "declined", + subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Dynamic Redirect + c.redirectWithStatus = function (status, subscriptionId, errorMessage, rect) { + var params = new URLSearchParams(); + params.set("status", status); + params.set("session", sessionStorage.getItem("paymentSessionToken")); + params.set("fraud_id", c.data.recordid); + if (subscriptionId) params.set("subscription_id", subscriptionId); + //if (errorMessage) params.set('error', encodeURIComponent(errorMessage)); + var redirectUrl = PAYMENT_SUCCESS_PAGE + "&" + params.toString(); + $window.location.href = redirectUrl; + }; + + // Error and Success Display + c.displayFieldError = function (fieldName, message) { + var errorElementId = { + cardNumber: "card-number-errors", + cardExpiry: "card-expiry-errors", + cardCvc: "card-cvc-errors", + address: "address-errors", + }[fieldName]; + var errorElement = $window.document.getElementById(errorElementId); + if (errorElement) { + errorElement.textContent = message; + errorElement.style.display = message ? "block" : "none"; + } + }; + + c.clearError = function () { + var errorElement = $window.document.getElementById("card-errors"); + if (errorElement) { + errorElement.textContent = ""; + errorElement.style.display = "none"; + } + }; + + c.showError = function (message) { + var errorElement = $window.document.getElementById("card-errors"); + if (errorElement) { + errorElement.textContent = message; + errorElement.style.display = "block"; + } + logMessage("ERROR", message); + }; + + c.showSuccess = function (message) { + var successElement = $window.document.getElementById("success-message"); + if (successElement) { + successElement.textContent = message; + successElement.style.display = "block"; + } + logMessage("INFO", message); + }; + + c.clearForm = function () { + if (c.cardNumber) c.cardNumber.clear(); + if (c.cardExpiry) c.cardExpiry.clear(); + if (c.cardCvc) c.cardCvc.clear(); + var cardholderNameInput = + $window.document.getElementById("cardholder-name"); + if (cardholderNameInput) cardholderNameInput.value = ""; + c.fieldErrors = { + cardNumber: "", + cardExpiry: "", + cardCvc: "", + address: "", + }; + c.errorMessage = ""; + c.clearError(); + }; + + // Form Validation + c.isFormValid = function () { + var cardholderName = + $window.document.getElementById("cardholder-name")?.value?.trim() || ""; + var email = $window.document.getElementById("email")?.value?.trim() || ""; + return ( + c.stripe && + c.cardNumber && + c.cardExpiry && + c.cardCvc && + c.addressData?.address && + cardholderName && + email && + !c.isProcessing + ); + }; + + c.hasFieldErrors = function () { + return Object.values(c.fieldErrors).some(function (error) { + return error; + }); + }; + + // Prevent Navigation with Alert + function handleNavigation(event) { + if (c.isProcessing || !c.paymentCompleted) { + event.preventDefault(); + event.returnValue = + "Payment is in progress. Are you sure you want to leave?"; + } + } + + // Cleanup + function cleanup() { + var form = $window.document.getElementById("payment-form"); + if (form) form.removeEventListener("submit", c.handleSubmit); + + if (c.cardNumber) c.cardNumber.destroy(); + if (c.cardExpiry) c.cardExpiry.destroy(); + if (c.cardCvc) c.cardCvc.destroy(); + if (c.addressElement) c.addressElement.destroy(); + + if (c.inactivityTimeout) $timeout.cancel(c.inactivityTimeout); + if (c.paymentTimeout) $timeout.cancel(c.paymentTimeout); + + ["mousemove", "keydown", "touchstart", "scroll"].forEach(function (event) { + $window.removeEventListener(event, resetInactivityTimer); + }); + + $window.removeEventListener("beforeunload", handleNavigation); + } + + // Widget Initialization + c.$onInit = function () { + initializeSession(); + startTimers(); + $window.addEventListener("beforeunload", handleNavigation); + + $timeout(function () { + c.initializeStripe() + .then(c.initializeStripeElements) + .catch(function (error) { + c.errorMessage = "Failed to load payment system."; + c.showError(c.errorMessage); + c.redirectWithStatus("declined", null, c.errorMessage); + }); + }, 100); + }; + + c.handlePaymentMethodSuccess = function (paymentMethod) { + const paymentData = { + payment_method_id: paymentMethod.id, + amount: c.data.amount, // Amount in cents + currency: c.data.currency || "usd", + customer_info: { + id: "cus_STfKPEu4DJT06O", + name: c.data.customerName || "Customer", + email: c.data.customerEmail || null, + }, + metadata: { + order_id: c.data.orderId || null, + recordid: "4c9a6f4483526e50cf0977447daad38b", + description: c.data.description || "ServiceNow Payment", + }, + }; + + c.server + .get({ + action: "createPaymentIntent", + paymentData: paymentData, + }) + .then(function (response) { + // console.log(response); + // console.log("Line 256" + response.data.response.success); + safeApply(function () { + if (response.data.response.success) { + // Check if payment requires additional authentication + if ( + response.data.response.requires_action && + response.data.response.client_secret + ) { + c.handlePaymentAction( + response.data.response.client_secret, + response.data.response.payment_intent_id + ); + } else if (response.data.response.status === "succeeded") { + // Payment completed successfully + c.successMessage = "Payment completed successfully!"; + c.clearForm(); + c.isProcessing = false; + c.redirectWithStatus( + "MonkeyLampEchoFuzz9RainyTaxiGlueXenonWaltz32GhostPieClapMint", + subscriptionId, + "", + c.recordid + ); + } else { + // Payment is processing + c.successMessage = "Payment is being processed..."; + c.isProcessing = false; + } + } else { + c.errorMessage = + response.data.response.error || + "Payment failed. Please try again."; + c.isProcessing = false; + c.redirectWithStatus( + "declined", + subscriptionId, + c.errorMessage, + c.recordid + ); + } + }); + }) + .catch(function (error) { + // console.error("Server error:", error); + safeApply(function () { + c.errorMessage = "Server error. Please try again."; + c.isProcessing = false; + c.redirectWithStatus( + "declined", + subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Updated handlePaymentAction for 3D Secure + c.handlePaymentAction = function (clientSecret, paymentIntentId) { + c.stripe + .confirmCardPayment(clientSecret) + .then(function (result) { + safeApply(function () { + if (result.error) { + c.errorMessage = result.error.message; + } else { + // Payment confirmed successfully + c.successMessage = + "Payment confirmed successfully! Payment ID: " + paymentIntentId; + c.clearForm(); + c.redirectWithStatus( + "MonkeyLampEchoFuzz9RainyTaxiGlueXenonWaltz32GhostPieClapMint", + subscriptionId, + "", + c.recordid + ); + // Optional: Notify server that payment was confirmed + c.server.get({ + action: "confirmPaymentSuccess", + payment_intent_id: paymentIntentId, + }); + } + c.isProcessing = false; + }); + }) + .catch(function (error) { + // console.error("Payment confirmation error:", error); + safeApply(function () { + c.errorMessage = "Payment confirmation failed. Please try again."; + c.isProcessing = false; + c.redirectWithStatus( + "declined", + subscriptionId, + c.errorMessage, + c.recordid + ); + }); + }); + }; + + // Widget Cleanup + c.$onDestroy = cleanup; + + $scope.$on("$destroy", cleanup); +}; diff --git a/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_css.css b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_css.css new file mode 100644 index 0000000000..e7c100fdd7 --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_css.css @@ -0,0 +1,450 @@ +* { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", sans-serif; + box-sizing: border-box; +} + +body { + background: #fafbfc; + margin: 0; + padding: 0; + color: #1a1f36; + -webkit-font-smoothing: antialiased; + line-height: 1.6; +} + +.main-container { + display: flex; + min-height: 100vh; + margin: 0 auto; + background: white; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.04); +} + +@media (max-width: 1024px) { + .main-container { + flex-direction: column; + max-width: none; + box-shadow: none; + } +} + +/* Left Panel - Order Summary */ +.order-summary { + background: #0e324a; + width: 75%; + padding: 48px 40px; + border-right: 1px solid #e6ebf1; + display: flex; + flex-direction: column; +} + +@media (max-width: 1024px) { + .order-summary { + width: 100%; + border-right: none; + border-bottom: 1px solid #e6ebf1; + padding: 32px 24px; + } +} + +.logo-section { + margin-bottom: 40px; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.logo { + height: 48px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 4rem; + color: white; + margin-right: 12px; + transition: all 0.2s ease; + cursor: pointer; +} + +.logo-text { + font-size: 20px; + font-weight: 600; + color: #1a1f36; +} + +.product-info { + margin-bottom: 32px; +} + +.product-title { + font-size: 3.5rem; + font-weight: 600; + color: white; + margin-bottom: 8px; +} + +.product-description { + font-size: 2ren; + color: #6b7c93; + margin-bottom: 24px; +} + +.price-display { + display: flex; + align-items: baseline; + margin-bottom: 32px; +} + +.price-amount { + font-size: 48px; + font-weight: 500; + color: #3fea23; + margin-right: 8px; + transition: all 1s ease; +} + +.price-amount:hover { + font-size: 51px; + font-weight: 600; +} + +.price-period { + font-size: 18px; + color: #d8d8d8; + font-weight: 500; +} + +.order-breakdown { + flex: 1; +} + +.breakdown-toggle { + background: white; + border: 1px solid #e6ebf1; + border-radius: 8px; + padding: 16px 20px; + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 16px; +} + +.breakdown-toggle:hover { + border-color: #c7d2fe; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); +} + +.breakdown-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 700; + color: #1a1f36; +} + +.breakdown-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease; + color: #6b7c93; +} + +.breakdown-icon.expanded { + transform: rotate(180deg); +} + +.breakdown-content { + background: white; + border: 1px solid #e6ebf1; + border-radius: 8px; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease-out; +} + +.breakdown-content.expanded { + max-height: 400px; + margin-bottom: 16px; +} + +.breakdown-inner { + padding: 20px; +} + +.breakdown-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 12px 0; + border-bottom: 1px solid #f6f9fc; +} + +.breakdown-item:last-child { + border-bottom: none; +} + +.item-details { + flex: 1; +} + +.item-name { + font-size: 14px; + font-weight: 600; + color: #1a1f36; + margin-bottom: 2px; +} + +.item-description { + font-size: 13px; + color: #6b7c93; +} + +.item-price { + font-size: 14px; + font-weight: 500; + color: #1a1f36; + white-space: nowrap; +} + +.total-section { + background: white; + border: 1px solid #e6ebf1; + border-radius: 8px; + padding: 20px; +} + +.total-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; +} + +.total-label { + font-size: 16px; + font-weight: 600; + color: #1a1f36; +} + +.total-amount { + font-size: 20px; + font-weight: 700; + color: #1a1f36; +} + +/* Right Panel - Payment Form */ +.payment-section { + width: 60%; + padding: 48px 40px; + background: white; +} + +@media (max-width: 1024px) { + .payment-section { + width: 100%; + padding: 32px 24px; + } +} + +.payment-header { + margin-bottom: 32px; +} + +.payment-title { + font-size: 24px; + font-weight: 600; + color: #1a1f36; + margin-bottom: 8px; +} + +.payment-subtitle { + font-size: 16px; + color: #6b7c93; +} + +.form-section { + background: #fafbfc; + border: 1px solid #e6ebf1; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + transition: all 0.2s ease; +} + +.form-section:focus-within { + border-color: #635bff; + box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.1); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group:last-child { + margin-bottom: 0; +} + +.form-label { + display: block; + font-size: 14px; + font-weight: 500; + color: #1a1f36; + margin-bottom: 8px; +} + +.form-input { + width: 100%; + padding: 12px 16px; + border: 1px solid #e6ebf1; + border-radius: 6px; + font-size: 16px; + color: #1a1f36; + background: white; + transition: all 0.2s ease; +} + +.form-input:focus { + outline: none; + border-color: #635bff; + box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.1); +} + +.form-input::placeholder { + color: #a3acb9; +} + +.stripe-element { + padding: 12px 16px; + border: 1px solid #e6ebf1; + border-radius: 6px; + background: white; + transition: all 0.2s ease; +} + +.stripe-element:focus-within { + border-color: #635bff; + box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.1); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +@media (max-width: 640px) { + .form-row { + grid-template-columns: 1fr; + } +} + +.card-icons { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.card-icon { + height: 20px; + width: auto; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.card-icon:hover { + opacity: 1; +} + +.submit-button { + width: 100%; + background: linear-gradient(135deg, #635bff 0%, #5469d4 100%); + color: white; + padding: 16px 24px; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 2px 4px rgba(99, 91, 255, 0.2); +} + +.submit-button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(99, 91, 255, 0.3); +} + +.submit-button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.security-note { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 16px; + font-size: 13px; + color: #6b7c93; +} + +.security-icon { + width: 16px; + height: 16px; + color: #10b981; +} + +.error-message { + color: #dc2626; + font-size: 13px; + margin-top: 4px; +} + +.success-message { + color: #10b981; + font-size: 13px; + margin-top: 4px; +} + +.powered-by { + text-align: center; + margin-top: 24px; + font-size: 13px; + color: #a3acb9; +} + +.powered-by a { + color: #635bff; + text-decoration: none; + font-weight: 500; +} + +.powered-by a:hover { + text-decoration: underline; +} + +/* Animations */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} diff --git a/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_html.html b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_html.html new file mode 100644 index 0000000000..7a6fbf008d --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_html.html @@ -0,0 +1,338 @@ + + + + + + Subscribe to Premium Plan + + + +
+ +
+
+ +
+
+ +
+

{{c.data.productname}}

+

{{c.data.productdesc}}

+ +
+ {{c.data.total_amount}} + {{c.data.billing_cycle}} +
+
+ +
+
+
+ Order Breakdown +
+ + + +
+
+
+ +
+
+
+
+
{{c.data.productname}}
+
{{c.data.productedition}}
+
+
{{c.data.productcost}} (Per Unit)
+
+ +
+
+
Users
+
+
+
{{c.data.total_users}}
+
+ +
+
+
Promocode
+
{{c.data.promo_code}}
+
+

{{c.data.discount_amount}}

+
+ +
+
+
Subtotal
+
+
{{c.data.total_amount}}
+
+
+
+ +
+
+
Total
+
{{c.data.total_amount}}
+
+
+
+
+ + +
+
+

Payment details

+

+ Complete your subscription to get started +

+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+ Visa + Mastercard + American Express +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+
+ +
+
+ +
+
+
+
+ + + +
+ + + + Your payment information is secure and encrypted +
+
+
+
+ + + + + diff --git a/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_server_code.js b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_server_code.js new file mode 100644 index 0000000000..476f01ee6b --- /dev/null +++ b/Modern Development/Service Portal Widgets/Stripe Payment Widget/Stripe_Payment_server_code.js @@ -0,0 +1,251 @@ +(function () { + data.stripeKey = gs.getProperty("Stripe_element_key"); + + // Transaction history sys_id + var recordSysId = input.record; + var recordType = input.type; // Fixed typo: recordtye -> recordType + data.typeofrecord = recordType; + data.recordid = recordSysId; + + var stripeProcessor = new StripePaymentProcessor(); + + // Handle different client actions + if (input.action === "createSubscription") { + try { + var result = stripeProcessor.createSubscriptionFromClient( + input.subscriptionData, + input.subscriptionData.recordid + ); + data.response = { + success: true, // Added success flag + data: result, + }; + } catch (error) { + gs.error("Error creating subscription: " + error.message); + data.response = { + success: false, + error: error.message || "Subscription creation failed", + }; + } + return; // Exit early after handling action + } else if (input.action === "confirm3DSecurePayment") { + try { + var result = stripeProcessor.confirm3DSecurePayment( + input.payment_intent_id, + input.recordid + ); + data.response = { + success: true, + data: result, + }; + } catch (error) { + gs.error("Error confirming 3D Secure payment: " + error.message); + data.response = { + success: false, + error: error.message || "3D Secure confirmation failed", + }; + } + return; // Exit early after handling action + } else if (input.action === "checkPaymentStatus") { + try { + var result = stripeProcessor.checkPaymentStatus(input.payment_intent_id); + data.response = { + success: true, // Added success flag + status: result.status, + error: result.error || null, + }; + } catch (error) { + gs.error("Error checking payment status: " + error.message); + data.response = { + success: false, + status: "error", + error: error.message || "Payment status check failed", + }; + } + return; // Exit early after handling action + } else if (input.action === "checkSubscriptionStatus") { + try { + var result = stripeProcessor.checkSubscriptionStatus( + input.subscription_id + ); + data.response = { + success: true, // Added success flag + status: result.status, + error: result.error || null, + }; + } catch (error) { + gs.error("Error checking subscription status: " + error.message); + data.response = { + success: false, + status: "error", + error: error.message || "Subscription status check failed", + }; + } + return; // Exit early after handling action + } else if (input.action === "checkFinalPaymentConfirmation") { + try { + var result = stripeProcessor.checkFinalPaymentConfirmation( + input.subscription_id, + input.payment_intent_id, + input.recordid + ); + data.response = { + success: result.success, + data: result, + }; + } catch (error) { + gs.error("Error checking final payment confirmation: " + error.message); + data.response = { + success: false, + error: error.message || "Final payment confirmation check failed", + }; + } + return; // Exit early after handling action + } else if (input.action === "createPaymentIntent") { + try { + data.response = stripeProcessor.createPaymentIntent(input.paymentData); + if (!data.response.success) { + data.response.success = false; // Ensure success flag is set + } + } catch (error) { + gs.error("Error creating payment intent: " + error.message); + data.response = { + success: false, + error: error.message || "Payment intent creation failed", + }; + } + return; // Added return for consistency + } else if (input.action === "confirmPaymentSuccess") { + try { + data.response = stripeProcessor.confirmPaymentSuccess( + input.payment_intent_id + ); + if (!data.response.success) { + data.response.success = false; // Ensure success flag is set + } + } catch (error) { + gs.error("Error confirming payment success: " + error.message); + data.response = { + success: false, + error: error.message || "Payment confirmation failed", + }; + } + return; // Added return for consistency + } else if (input.action) { + // Handle unknown actions but only if action is provided + data.response = { + success: false, + error: "Unknown action: " + input.action, + }; + return; // Exit early for unknown actions + } + + // Default behavior: Load transaction history data + // Extracted common logic into a function to avoid duplication + function loadTransactionHistoryData() { + var grUTH = new GlideRecord("u_transaction_history"); + + // Fetch and populate data if record is found + if (recordSysId && grUTH.get(recordSysId)) { + data.product = grUTH.getDisplayValue("u_product"); + + if (data.product) { + var gr_PA = new GlideRecord("cmdb_software_product_model"); + if (gr_PA.get(grUTH.getValue("u_product"))) { + data.productedition = gr_PA.getValue("edition"); + data.productname = gr_PA.getValue("name"); + data.productcost = gr_PA.cost.getReferenceValue(); + data.productdesc = gr_PA.getValue("short_description"); + } + } + + // Fixed: Access field value correctly + data.productplan = + grUTH.getValue("u_product.edition") || grUTH.u_product.edition; + data.promo_code = grUTH.getDisplayValue("u_promo_code"); + + var billing_cycle = grUTH.getValue("u_billing_cycle"); + data.billing_cycle = billing_cycle == "year" ? "Per Year" : "Per Month"; + + data.discount_amount = grUTH.getValue("u_discount_amount"); + data.subscribed_on = grUTH.getValue("u_subscribed_on"); + data.status = grUTH.getValue("u_status"); + data.ends_on = grUTH.getValue("u_ends_on"); + data.customerid = grUTH.getValue("u_customerid"); + data.customer_stripeid = grUTH.getValue("u_customer_stripe_id"); + data.total_users = grUTH.getValue("u_total_users"); + data.total_amount = "$" + grUTH.getValue("u_total_amount"); + + // Only set upgradeplan if recordType is not specified (second branch behavior) + if (!recordType) { + data.upgradeplan = grUTH.getValue("u_upgrade_history"); + } + + // Get customer address data + var customerSysId = grUTH.getValue("u_customerid"); + if (customerSysId) { + var grCA = new GlideRecord("customer_account"); + if (grCA.get(customerSysId)) { + data.customer_name = grCA.getValue("name") || ""; + data.user_name = gs.getUserDisplayName(); + data.user_email = gs.getUser().getEmail(); + data.customer_email = grCA.getValue("email") || ""; + data.customer_phone = grCA.getValue("phone") || ""; + + var countryName = grCA.getValue("country"); + var stateName = grCA.getValue("state") || ""; + + var countryCode = "US"; // Default fallback + if (countryName) { + var grCountry = new GlideRecord("core_country"); + grCountry.addQuery("name", countryName); + grCountry.setLimit(1); // Only need one result + grCountry.query(); + + if (grCountry.next()) { + countryCode = grCountry.getValue("iso3166_2") || "US"; + } + } + + var stateCode = stateName; + + // Only apply state code mapping if recordType is not specified + if (!recordType) { + var sys_property = "stripe_state_code"; + var mappingJSON = gs.getProperty(sys_property, "{}"); + var mapping = {}; + + try { + mapping = JSON.parse(mappingJSON); + if (mapping[countryCode] && mapping[countryCode][stateName]) { + stateCode = mapping[countryCode][stateName]; + } + } catch (e) { + gs.warn("Invalid JSON in sys_property: " + sys_property); + } + } + + data.address = { + line1: grCA.getValue("street") || "", + city: grCA.getValue("city") || "", + state: stateCode || "", + postal_code: grCA.getValue("zip") || "", + country: countryCode, + }; + } else { + gs.warn("Customer account not found for sys_id: " + customerSysId); + data.error = "Customer record not found"; + } + } + } else { + // Handle case when record is not found + gs.warn( + "Transaction history record not found for sys_id: " + recordSysId + ); + data.error = "Transaction history record not found"; + } + } + + // Call the common function + loadTransactionHistoryData(); +})();