1function urlB64ToUint8Array(base64String) { 2 const padding = '='.repeat((4 - base64String.length % 4) % 4); 3 const base64 = (base64String + padding) 4 .replace(/\-/g, '+') 5 .replace(/_/g, '/'); 6 7 const rawData = window.atob(base64); 8 const outputArray = new Uint8Array(rawData.length); 9 10 for (let i = 0; i < rawData.length; ++i) { 11 outputArray[i] = rawData.charCodeAt(i); 12 } 13 return outputArray; 14 } 15 16class PushNotifications { 17 STATE_INITIALIZING = 0; 18 STATE_UNSUPPORTED = 1; 19 STATE_READY = 2; 20 STATE_WORKING = 3; 21 STATE_DENIED = 4; 22 STATE_FAILED_PUB_KEY = 5; 23 STATE_OTHER_ERROR = 6; 24 25 constructor() { 26 this.vapid_key_endpoint = "/api/notifications/resources/vapid-pub-key"; 27 this.applicationServerPublicKey = null; 28 this.swRegistration = null; 29 this.subscription = null; 30 this.lastState = null; 31 this.disabled = true; 32 this.innerHTML = "Initializing..."; 33 this.initialize(); 34 } 35 36 async initialize() { 37 await this.uiState(this.STATE_INITIALIZING); 38 if('serviceWorker' in navigator === false) return this.uiState(this.STATE_UNSUPPORTED); 39 if('PushManager' in window === false) return this.uiState(this.STATE_UNSUPPORTED); 40 41 try{ 42 const swReg = await navigator.serviceWorker.register('/ServiceWorker.js') 43 console.log("Service Worker is registered", swReg); 44 this.swRegistration = swReg; 45 await this.uiState(this.STATE_READY); 46 } catch(error) { 47 console.error('Service Worker Error', error); 48 await this.uiState(this.STATE_UNSUPPORTED); 49 } 50 } 51 52 async uiState(status = null) { 53 if(status !== null) this.lastState = status; 54 else status = this.lastState; 55 56 switch(status) { 57 case this.STATE_INITIALIZING: 58 this.disabled = true; 59 this.innerHTML = "<loading-spinner></loading-spinner> One moment..."; 60 break; 61 case this.STATE_UNSUPPORTED: 62 this.disabled = true; 63 this.innerHTML = "Unsupported Browser"; 64 break; 65 case this.STATE_READY: 66 this.disabled = false; 67 const sub = await this.swRegistration.pushManager.getSubscription(); 68 this.subscription = !(sub === null); 69 this.innerHTML = `${(this.subscription) ? "Disable" : "Enable"} Push Notifications`; 70 break; 71 case this.STATE_WORKING: 72 this.disabled = true; 73 this.innerHTML = "<loading-spinner></loading-spinner> Working..."; 74 break; 75 case this.STATE_DENIED: 76 this.disabled = true; 77 this.innerHTML = "Missing permission <help-span value='Please enable the notification permission for this site to continue.'></help-span>"; 78 this.status = "denied"; 79 break; 80 case this.STATE_FAILED_PUB_KEY: 81 this.disabled = false; 82 this.innerHTML = "Failed to fetch public key"; 83 break; 84 case this.STATE_OTHER_ERROR: 85 default: 86 this.disabled = false; 87 this.innerHTML = "Unknown Error"; 88 break; 89 } 90 91 document.body.dispatchEvent(new CustomEvent("PushNotificationStateUpdate")); 92 } 93 94 toggleSubscription() { 95 this.uiState(this.STATE_WORKING); 96 if(!this.subscription) return this.enroll(); 97 return this.unsub(); 98 } 99 100 async enroll() { 101 try { 102 const api = new AsyncFetch(this.vapid_key_endpoint, "GET", {}); 103 this.applicationServerPublicKey = await api.get(); 104 } catch (error) { 105 console.error("Failed to fetch this application's public key"); 106 this.uiState(this.STATE_FAILED_PUB_KEY); 107 } 108 109 let applicationServerKey; 110 let subscription; 111 try { 112 applicationServerKey = urlB64ToUint8Array(this.applicationServerPublicKey); 113 } catch(error) { 114 console.error(error); 115 this.uiState(this.STATE_OTHER_ERROR); 116 return; 117 } 118 try { 119 subscription = await this.swRegistration.pushManager.subscribe({ 120 userVisibleOnly: true, 121 applicationServerKey: applicationServerKey 122 }); 123 } catch (error) { 124 console.error(error); 125 this.uiState(this.STATE_OTHER_ERROR); 126 return; 127 } 128 129 try{ 130 await this.updateSubscriptionOnServer(subscription, "subscribed"); 131 this.subscription = true; 132 this.uiState(this.STATE_READY); 133 } catch (error) { 134 console.error("Failed to enroll", error); 135 this.uiState(this.STATE_OTHER_ERROR); 136 } 137 } 138 139 async unsub() { 140 const subscription = await this.swRegistration.pushManager.getSubscription(); 141 try{ 142 subscription.unsubscribe(); 143 } catch (error) { 144 console.error("There was an error unsubscribing", error); 145 } 146 this.updateSubscriptionOnServer(subscription, "unsubscribed"); 147 this.status = false;
148 this.uiState("ready"); 149 } 150 151 updateSubscriptionOnServer(subscription, state = "enrolled", userId = null) { 152 if(!userId) userId = "me"; 153 const api = new AsyncFetch(`/api/v1/user/${userId}/push/enrollment/${state}`,"POST",{}); 154 return api.submit(subscription); 155 } 156} 157 158class PushNotificationEnrollmentButton extends CustomButton { 159 connectedCallback() { 160 this.innerHTML = "<loading-spinner></loading-spinner> Loading..."; 161 super.connectedCallback(); 162 this.disabled = true; 163 164 this.actionMenu = this.parentNode.querySelector("action-menu"); 165 this.optionsTest = this.actionMenu?.querySelector("option[name='test']"); 166 167 this.addEventListener("click", () => { 168 window.Cobalt.PushNotificationInstance.toggleSubscription(); 169 }); 170 document.body.addEventListener("PushNotificationStateUpdate", this.updateSelf.bind(this)); 171 this.updateSelf(); 172 } 173 174 disconnectedCallback() { 175 // document.body.removeEventListener("PushNotificationStateUpdate", this.updateSelf.bind(this)); 176 } 177 178 updateSelf() { 179 this.disabled = window.Cobalt.PushNotificationInstance.disabled; 180 this.innerHTML = window.Cobalt.PushNotificationInstance.innerHTML || "Oops!"; 181 console.log(this.optionsTest); 182 this.optionsTest.disabled = this.disabled; 183 } 184 185 get disabled() { 186 return this.hasAttribute("disabled"); 187 } 188 189 set disabled(val) { 190 if(val) this.setAttribute("disabled", "disabled"); 191 else this.removeAttribute("disabled"); 192 } 193} 194 195window.Cobalt.PushNotificationInstance = new PushNotifications(); 196customElements.define("push-enrollment-button", PushNotificationEnrollmentButton);
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.