PageSourceSearch

https://smartsignpro.ai/assets/controllers/push_subscription_contr…d421f68c53c8f80d374876c2253763f66447a7ac.js

js smartsignpro.ai collected 2026-10-02 00:43:10 UTC 6,676 bytes, 214 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Handles browser push opt-in for both staff and customers. Registers the
4// service worker, requests notification permission, subscribes via the Push API,
5// and persists the subscription server-side. Degrades gracefully when the
6// browser lacks support or the user has blocked notifications.
7export default class extends Controller {
8  static targets = ["prompt", "enableButton", "blockedHint", "enabledHint", "unsupported", "errorHint"]
9  static values = {
10    publicKey: String,
11    subscribeUrl: String,
12    unsubscribeUrl: String,
13    context: String,
14    locationId: String,
15    orderId: String,
16    orderToken: String,
17    auto: Boolean
18  }
19
20  connect() {
21    if (!this.supported()) {
22      this.hide(this.prompt)
23      this.hide(this.blockedHint)
24      this.hide(this.enabledHint)
25      this.show(this.unsupported)
26      return
27    }
28    this.reflectState()
29  }
30
31  supported() {
32    return "serviceWorker" in navigator && "PushManager" in window && "Notification" in window
33  }
34
35  get prompt() {
36    return this.hasPromptTarget ? this.promptTarget : null
37  }
38
39  get blockedHint() {
40    return this.hasBlockedHintTarget ? this.blockedHintTarget : null
41  }
42
43  get enabledHint() {
44    return this.hasEnabledHintTarget ? this.enabledHintTarget : null
45  }
46
47  get unsupported() {
48    return this.hasUnsupportedTarget ? this.unsupportedTarget : null
49  }
50
51  get errorHint() {
52    return this.hasErrorHintTarget ? this.errorHintTarget : null
53  }
54
55  reflectState() {
56    const permission = Notification.permission
57    this.hide(this.errorHint)
58    if (permission === "granted") {
59      this.hide(this.prompt)
60      this.hide(this.blockedHint)
61      // Permission is granted, but a valid push subscription may still be
62      // missing (e.g. it rotated, or this browser has no push service). Only
63      // claim "enabled" once we've actually confirmed a subscription.
64      this.confirmSubscription()
65    } else if (permission === "denied") {
66      this.hide(this.prompt)
67      this.hide(this.enabledHint)
68      this.show(this.blockedHint)
69    } else {
70      this.show(this.prompt)
71      this.hide(this.blockedHint)
72      this.hide(this.enabledHint)
73    }
74  }
75
76  async confirmSubscription() {
77    const ok = await this.subscribe(true)
78    if (ok) {
79      this.hide(this.prompt)
80      this.hide(this.errorHint)
81      this.hide(this.unsupported)
82      this.show(this.enabledHint)
83    } else {
84      this.hide(this.enabledHint)
85      this.handleSubscribeFailure()
86    }
87  }
88
89  async enable() {
90    if (!this.supported() || !this.publicKeyValue) return
91
92    const permission = await Notification.requestPermission()
93    if (permission === "granted") {
94      const ok = await this.subscribe(false)
95      this.hide(this.prompt)
96      this.hide(this.blockedHint)
97      if (ok) {
98        this.hide(this.errorHint)
99        this.hide(this.unsupported)
100        this.show(this.enabledHint)
101      } else {
102        this.hide(this.enabledHint)
103        this.handleSubscribeFailure()
104      }
105    } else if (permission === "denied") {
106      this.hide(this.prompt)
107      this.hide(this.enabledHint)
108      this.show(this.blockedHint)
109    }
110  }
111
112  // The browser exposes the Push APIs but couldn't create a subscription. The
113  // most common cause is a privacy-focused browser with its push service off —
114  // notably Brave, which needs "Use Google services for push messaging" enabled
115  // (brave://settings/privacy). Show the actionable errorHint (which explains
116  // this) rather than the misleading "not supported" message.
117  handleSubscribeFailure() {
118    this.hide(this.unsupported)
119    if (this.errorHint) {
120      this.hide(this.prompt)
121      this.show(this.errorHint)
122    } else {
123      // No dedicated error slot: re-offer the prompt so the user can retry.
124      this.show(this.prompt)
125    }
126  }
127
128  // Registers the service worker and ensures a push subscription exists, then
129  // persists it server-side. Returns true only when a subscription is active.
130  async subscribe(silent) {
131    try {
132      const registration = await navigator.serviceWorker.register("/sw.js")
133      await navigator.serviceWorker.ready
134
135      let subscription = await registration.pushManager.getSubscription()
136
137      // On an explicit opt-in, drop any stale subscription first so we always
138      // re-subscribe against the current VAPID key. This recovers from a
139      // rotated key or a subscription the server no longer recognizes.
140      if (subscription && !silent) {
141        await subscription.unsubscribe()
142        subscription = null
143      }
144
145      if (!subscription) {
146        subscription = await registration.pushManager.subscribe({
147          userVisibleOnly: true,
148          applicationServerKey: this.urlBase64ToUint8Array(this.publicKeyValue)
149        })
150      }
151
152      const response = await this.persist(subscription)
153      return Boolean(subscription) && response
154    } catch (error) {
155      if (!silent) console.warn("Push subscription failed", error)
156      return false
157    }
158  }
159
160  async persist(subscription) {
161    const json = subscription.toJSON()
162    const body = {
163      endpoint: subscription.endpoint,
164      p256dh: json.keys ? json.keys.p256dh : null,
165      auth: json.keys ? json.keys.auth : null,
166      context: this.hasContextValue ? this.contextValue : "customer",
167      location_id: this.hasLocationIdValue ? this.locationIdValue : null,
168      order_id: this.hasOrderIdValue ? this.orderIdValue : null,
169      order_token: this.hasOrderTokenValue ? this.orderTokenValue : null
170    }
171
172    const response = await fetch(this.subscribeUrlValue, {
173      method: "POST",
174      credentials: "same-origin",
175      headers: {
176        "Content-Type": "application/json",
177        Accept: "application/json",
178        "X-CSRF-Token": this.csrfToken()
179      },
180      body: JSON.stringify({ push_subscription: body })
181    })
182    return response.ok
183  }
184
185  dismiss() {
186    this.hide(this.prompt)
187  }
188
189  urlBase64ToUint8Array(base64String) {
190    const padding = "=".repeat((4 - (base64String.length % 4)) % 4)
191    const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/")
192    const rawData = window.atob(base64)
193    const output = new Uint8Array(rawData.length)
194    for (let i = 0; i < rawData.length; ++i) output[i] = rawData.charCodeAt(i)
195    return output
196  }
197
198  csrfToken() {
199    const meta = document.querySelector('meta[name="csrf-token"]')
200    return meta ? meta.content : ""
201  }
202
203  show(el) {
204    if (!el) return
205    el.classList.remove("hidden")
206    el.style.display = el.classList.contains("alert") ? "flex" : ""
207  }
208
209  hide(el) {
210    if (!el) return
211    el.classList.add("hidden")
212    el.style.display = "none"
213  }
214};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.