diff --git a/core/js/dist/login.js b/core/js/dist/login.js
index f1b4f34e195d6aed9062bc91b466458b2dd5beb7..7c8919f19c07c60e681cd4a317dc3a2ef1036d76 100644
Binary files a/core/js/dist/login.js and b/core/js/dist/login.js differ
diff --git a/core/js/dist/login.js.map b/core/js/dist/login.js.map
index 425e595de23c096008b4613ae243b652a8f98b85..aefe2010a863c3dbfa6b3f173afe9f3da2d4a298 100644
Binary files a/core/js/dist/login.js.map and b/core/js/dist/login.js.map differ
diff --git a/core/js/dist/main.js b/core/js/dist/main.js
index f0e244ced7679c4abc5ca9d4ec6d9bb218d01f8d..2b0a153464c439c9f52cc73589d5860f93ccd2e5 100644
Binary files a/core/js/dist/main.js and b/core/js/dist/main.js differ
diff --git a/core/js/dist/main.js.map b/core/js/dist/main.js.map
index 5d55cc3f2a0ddfc20685632068c101e1f25aa24b..038fc429e11b5dfbec52bc8f0416a14b67bb40ca 100644
Binary files a/core/js/dist/main.js.map and b/core/js/dist/main.js.map differ
diff --git a/core/js/dist/maintenance.js b/core/js/dist/maintenance.js
index 2c6060884167537e7e85616b8451fd534b4f6c5e..6c90596b97eafef4a00f99509c3578bf0b7504b6 100644
Binary files a/core/js/dist/maintenance.js and b/core/js/dist/maintenance.js differ
diff --git a/core/js/dist/maintenance.js.map b/core/js/dist/maintenance.js.map
index b7d69ddad8b99648b4cef9a3e9a481a67c0a3299..2468a3d2a24de97460858981fd6339fe04251735 100644
Binary files a/core/js/dist/maintenance.js.map and b/core/js/dist/maintenance.js.map differ
diff --git a/core/js/dist/recommendedapps.js b/core/js/dist/recommendedapps.js
index d6b920e787392996af9e2e09af03f5db2d8fee17..515ff59e71e6d57ff9ad4f66271736203a1e2b91 100644
Binary files a/core/js/dist/recommendedapps.js and b/core/js/dist/recommendedapps.js differ
diff --git a/core/js/dist/recommendedapps.js.map b/core/js/dist/recommendedapps.js.map
index 38821ee9952ddf715dec9c603d80d1e0f1a2a8ce..5a9e34e9b92aece98177bca9c0201240aeedb164 100644
Binary files a/core/js/dist/recommendedapps.js.map and b/core/js/dist/recommendedapps.js.map differ
diff --git a/core/src/session-heartbeat.js b/core/src/session-heartbeat.js
index 49cf547aa362d15add66cbfbc6b070b20d11652f..88c519e1dde0cf67963a4d1ae9341e27f983e878 100644
--- a/core/src/session-heartbeat.js
+++ b/core/src/session-heartbeat.js
@@ -20,6 +20,7 @@
  */
 
 import $ from 'jquery'
+import { emit } from '@nextcloud/event-bus'
 
 import { generateUrl } from './OC/routing'
 import OC from './OC'
@@ -54,6 +55,34 @@ const getInterval = () => {
 	)
 }
 
+const getToken = async() => {
+	const url = generateUrl('/csrftoken')
+
+	// Not using Axios here as Axios is not stubbable with the sinon fake server
+	// see https://stackoverflow.com/questions/41516044/sinon-mocha-test-with-async-ajax-calls-didnt-return-promises
+	// see js/tests/specs/coreSpec.js for the tests
+	const resp = await $.get(url)
+
+	return resp.token
+}
+
+const poll = async() => {
+	try {
+		const token = await getToken()
+		setRequestToken(token)
+	} catch (e) {
+		console.error('session heartbeat failed', e)
+	}
+}
+
+const startPolling = () => {
+	const interval = setInterval(poll, getInterval() * 1000)
+
+	console.info('session heartbeat polling started')
+
+	return interval
+}
+
 /**
  * Calls the server periodically to ensure that session and CSRF
  * token doesn't expire
@@ -63,12 +92,35 @@ export const initSessionHeartBeat = () => {
 		console.info('session heartbeat disabled')
 		return
 	}
+	let interval = startPolling()
+
+	window.addEventListener('online', async() => {
+		console.info('browser is online again, resuming heartbeat')
+		interval = startPolling()
+		try {
+			await poll()
+			console.info('session token successfully updated after resuming network')
 
-	setInterval(() => {
-		$.ajax(generateUrl('/csrftoken'))
-			.then(resp => setRequestToken(resp.token))
-			.fail(e => {
-				console.error('session heartbeat failed', e)
+			// Let apps know we're online and requests will have the new token
+			emit('networkOnline', {
+				success: true
 			})
-	}, getInterval() * 1000)
+		} catch (e) {
+			console.error('could not update session token after resuming network', e)
+
+			// Let apps know we're online but requests might have an outdated token
+			emit('networkOnline', {
+				success: false
+			})
+		}
+	})
+	window.addEventListener('offline', () => {
+		console.info('browser is offline, stopping heartbeat')
+
+		// Let apps know we're offline
+		emit('networkOffline', {})
+
+		clearInterval(interval)
+		console.info('session heartbeat polling stopped')
+	})
 }