diff --git a/.gitignore b/.gitignore
index 978c511aa1f259d53646f17e9551b88dcc11eb5f..724a02d35ec519dd5fc690430e98ff51d71bbc98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,9 +75,6 @@ mobile/Preview.html
 # /!\ KEEP THIS SECTION THE LAST ONE
 !.gitkeep
 
-#Report bundle analyser
-public/
-
 # CSS
 src/styles/index.css
 src/styles/index.css.map
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9e93bd20a34710f3b859cde1b560b846400d1a8c..dcb2207f4c2d3818afac11c4d71708ec949624ff 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -134,7 +134,9 @@
     "pseudonymisées",
     "reduxjs",
     "Reinit",
+    "sendmail",
     "SHARAPOWATT",
+    "shortcodes",
     "splashscreen",
     "swipeable",
     "Swipeable",
diff --git a/manifest.webapp b/manifest.webapp
index bab84280f862fdf7ea1424e350fd26bf7555c25e..d9ba906693498d74e9c344fa0be745dcbf62a9fc 100644
--- a/manifest.webapp
+++ b/manifest.webapp
@@ -162,6 +162,11 @@
     "url": "https://www.grandlyon.com/"
   },
   "routes": {
+    "/public": {
+      "folder": "/public",
+      "index": "index.html",
+      "public": true
+    },
     "/": {
       "folder": "/",
       "index": "index.html",
diff --git a/src/components/Options/Unsubscribe/UnSubscribeView.spec.tsx b/src/components/Options/Unsubscribe/UnSubscribeView.spec.tsx
deleted file mode 100644
index 790c171bbc6401b1f9b6ef760c972b1b40e4f55c..0000000000000000000000000000000000000000
--- a/src/components/Options/Unsubscribe/UnSubscribeView.spec.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import userEvent from '@testing-library/user-event'
-import React from 'react'
-import { Provider } from 'react-redux'
-import * as profileActions from 'store/profile/profile.slice'
-import { createMockEcolyoStore } from 'tests/__mocks__/store'
-import UnSubscribeView from './UnSubscribeView'
-
-jest.mock('components/Header/CozyBar', () => 'mock-cozybar')
-jest.mock('components/Header/Header', () => 'mock-header')
-jest.mock('components/Content/Content', () => 'mock-content')
-
-const mockUpdateProfile = jest.fn()
-jest.mock('services/profile.service', () => {
-  return jest.fn(() => ({
-    updateProfile: mockUpdateProfile,
-  }))
-})
-const updateProfileSpy = jest.spyOn(profileActions, 'updateProfile')
-const mockedNavigate = jest.fn()
-jest.mock('react-router-dom', () => ({
-  ...jest.requireActual('react-router-dom'),
-  useNavigate: () => mockedNavigate,
-}))
-
-describe('UnSubscribe component', () => {
-  const store = createMockEcolyoStore()
-  beforeEach(() => {
-    jest.clearAllMocks()
-  })
-
-  it('should be rendered correctly', () => {
-    const { container } = render(
-      <Provider store={store}>
-        <UnSubscribeView />
-      </Provider>
-    )
-    expect(container).toMatchSnapshot()
-  })
-
-  it('should click on button and deactivate report', async () => {
-    render(
-      <Provider store={store}>
-        <UnSubscribeView />
-      </Provider>
-    )
-    await userEvent.click(screen.getByRole('button'))
-    expect(updateProfileSpy).toHaveBeenCalledWith({
-      sendAnalysisNotification: false,
-    })
-  })
-})
diff --git a/src/components/Options/Unsubscribe/UnSubscribeView.tsx b/src/components/Options/Unsubscribe/UnSubscribeView.tsx
deleted file mode 100644
index f67dbdc7d0a92bff0faa7169dbefc07dfd120981..0000000000000000000000000000000000000000
--- a/src/components/Options/Unsubscribe/UnSubscribeView.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Button } from '@material-ui/core'
-import BearIcon from 'assets/icons/visu/duelResult/CHALLENGE0001-0.svg'
-import StyledIcon from 'components/CommonKit/Icon/StyledIcon'
-import Content from 'components/Content/Content'
-import CozyBar from 'components/Header/CozyBar'
-import Header from 'components/Header/Header'
-import { useI18n } from 'cozy-ui/transpiled/react/I18n'
-import React, { useState } from 'react'
-import { useNavigate } from 'react-router-dom'
-import { useAppDispatch } from 'store/hooks'
-import { updateProfile } from 'store/profile/profile.slice'
-import './unSubscribeView.scss'
-
-const UnSubscribeView = () => {
-  const { t } = useI18n()
-  const navigate = useNavigate()
-  const dispatch = useAppDispatch()
-  const [headerHeight, setHeaderHeight] = useState<number>(0)
-  const unSubscribe = async () => {
-    dispatch(updateProfile({ sendAnalysisNotification: false }))
-    navigate('/consumption')
-  }
-
-  return (
-    <>
-      <CozyBar titleKey="common.title_analysis" />
-      <Header
-        setHeaderHeight={setHeaderHeight}
-        desktopTitleKey="common.title_analysis"
-      />
-      <Content heightOffset={headerHeight}>
-        <div className="unsubscribe-container">
-          <StyledIcon className="profile-icon" icon={BearIcon} size={250} />
-
-          <div className="text-20-bold head">{t('unsubscribe.title')}</div>
-          <div className="text-16-normal question">
-            {t('unsubscribe.content')}
-          </div>
-          <Button
-            aria-label={t('unsubscribe.button_accessibility')}
-            onClick={() => unSubscribe()}
-            variant="contained"
-            classes={{
-              root: 'btnPrimary',
-              label: 'text-18-bold',
-            }}
-          >
-            {t('unsubscribe.button_text')}
-          </Button>
-        </div>
-      </Content>
-    </>
-  )
-}
-
-export default UnSubscribeView
diff --git a/src/components/Options/Unsubscribe/Unsubscribe.spec.tsx b/src/components/Options/Unsubscribe/Unsubscribe.spec.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a014ff8cb0b1b83f6483b2673a042a1391bf78be
--- /dev/null
+++ b/src/components/Options/Unsubscribe/Unsubscribe.spec.tsx
@@ -0,0 +1,31 @@
+import { render, screen, waitFor } from '@testing-library/react'
+import { userEvent } from '@testing-library/user-event'
+import React from 'react'
+import Unsubscribe from './Unsubscribe'
+
+const mockUpdateProfile = jest.fn().mockResolvedValue('')
+jest.mock('services/profile.service', () => {
+  return jest.fn(() => ({
+    updateProfile: mockUpdateProfile,
+  }))
+})
+
+describe('Unsubscribe component', () => {
+  it('should be rendered correctly', async () => {
+    const { container } = render(<Unsubscribe />)
+    await waitFor(() => null, { container })
+    expect(container).toMatchSnapshot()
+  })
+
+  it('should click the subscribe button', async () => {
+    const { container } = render(<Unsubscribe />)
+    await waitFor(() => null, { container })
+    const [subscribeBtn] = screen.getAllByRole('button')
+    await userEvent.click(subscribeBtn)
+    expect(mockUpdateProfile).toHaveBeenCalled()
+
+    // then should only display one button
+    const buttons = screen.getAllByRole('button')
+    expect(buttons.length).toBe(1)
+  })
+})
diff --git a/src/components/Options/Unsubscribe/Unsubscribe.tsx b/src/components/Options/Unsubscribe/Unsubscribe.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c5032fbdce8bce2b91da87985d616b54106f5d55
--- /dev/null
+++ b/src/components/Options/Unsubscribe/Unsubscribe.tsx
@@ -0,0 +1,80 @@
+import { Button } from '@material-ui/core'
+import * as Sentry from '@sentry/react'
+import BearIcon from 'assets/icons/visu/duelResult/CHALLENGE0001-0.svg'
+import StyledIcon from 'components/CommonKit/Icon/StyledIcon'
+import Loader from 'components/Loader/Loader'
+import { useClient } from 'cozy-client'
+import { useI18n } from 'cozy-ui/transpiled/react/I18n'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import ProfileService from 'services/profile.service'
+import './unsubscribe.scss'
+
+const Unsubscribe = () => {
+  const { t } = useI18n()
+  const client = useClient()
+
+  const [isLoading, setIsLoading] = useState(true)
+  const [status, setStatus] = useState<
+    'subscribed' | 'error' | 'unsubscribed'
+  >()
+
+  const profileService = useMemo(() => new ProfileService(client), [client])
+
+  const updateSubscription = useCallback(
+    (value: boolean) => {
+      setIsLoading(true)
+
+      profileService
+        .updateProfile({ sendAnalysisNotification: value })
+        .then(() => {
+          setStatus(value ? 'subscribed' : 'unsubscribed')
+        })
+        .catch(err => {
+          setStatus('error')
+          console.error(err)
+          Sentry.captureException('Failed to unsubscribe')
+        })
+        .finally(() => {
+          setIsLoading(false)
+        })
+    },
+    [profileService]
+  )
+
+  useEffect(() => {
+    updateSubscription(false)
+  }, [updateSubscription])
+
+  return (
+    <div className="unsubscribe-container">
+      {isLoading && <Loader />}
+      {!isLoading && (
+        <>
+          <StyledIcon className="profile-icon" icon={BearIcon} size={250} />
+          <div className="text-20-bold head">{t(`unsubscribe.${status}`)}</div>
+          {status === 'unsubscribed' && (
+            <div className="text-16-normal question">
+              {t('unsubscribe.content')}
+            </div>
+          )}
+          {status !== 'subscribed' && status !== 'error' && (
+            <Button
+              className="btnPrimary"
+              onClick={() => updateSubscription(true)}
+            >
+              {t('unsubscribe.button_subscribe')}
+            </Button>
+          )}
+          <Button
+            className="btnSecondary"
+            onClick={() => window.location.replace('/')}
+          >
+            {t('unsubscribe.button_home')}
+          </Button>
+        </>
+      )}
+    </div>
+  )
+}
+
+export default Unsubscribe
diff --git a/src/components/Options/Unsubscribe/__snapshots__/UnSubscribeView.spec.tsx.snap b/src/components/Options/Unsubscribe/__snapshots__/UnSubscribeView.spec.tsx.snap
deleted file mode 100644
index 85efc0cad5e2d2420047f871af41b2bef8bb7a84..0000000000000000000000000000000000000000
--- a/src/components/Options/Unsubscribe/__snapshots__/UnSubscribeView.spec.tsx.snap
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`UnSubscribe component should be rendered correctly 1`] = `
-<div>
-  <mock-cozybar
-    titlekey="common.title_analysis"
-  />
-  <mock-header
-    desktoptitlekey="common.title_analysis"
-  />
-  <mock-content
-    heightoffset="0"
-  >
-    <div
-      class="unsubscribe-container"
-    >
-      <svg
-        aria-hidden="true"
-        class="profile-icon styles__icon___23x3R"
-        height="250"
-        width="250"
-      >
-        <use
-          xlink:href="#test-file-stub"
-        />
-      </svg>
-      <div
-        class="text-20-bold head"
-      >
-        unsubscribe.title
-      </div>
-      <div
-        class="text-16-normal question"
-      >
-        unsubscribe.content
-      </div>
-      <button
-        aria-label="unsubscribe.button_accessibility"
-        class="MuiButtonBase-root MuiButton-root btnPrimary MuiButton-contained"
-        tabindex="0"
-        type="button"
-      >
-        <span
-          class="MuiButton-label text-18-bold"
-        >
-          unsubscribe.button_text
-        </span>
-        <span
-          class="MuiTouchRipple-root"
-        />
-      </button>
-    </div>
-  </mock-content>
-</div>
-`;
diff --git a/src/components/Options/Unsubscribe/__snapshots__/Unsubscribe.spec.tsx.snap b/src/components/Options/Unsubscribe/__snapshots__/Unsubscribe.spec.tsx.snap
new file mode 100644
index 0000000000000000000000000000000000000000..181c853d2520dfb3a2e4f02c4c43010138d1bc11
--- /dev/null
+++ b/src/components/Options/Unsubscribe/__snapshots__/Unsubscribe.spec.tsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Unsubscribe component should be rendered correctly 1`] = `
+<div>
+  <div
+    class="unsubscribe-container"
+  >
+    <svg
+      aria-hidden="true"
+      class="profile-icon styles__icon___23x3R"
+      height="250"
+      width="250"
+    >
+      <use
+        xlink:href="#test-file-stub"
+      />
+    </svg>
+    <div
+      class="text-20-bold head"
+    >
+      unsubscribe.unsubscribed
+    </div>
+    <div
+      class="text-16-normal question"
+    >
+      unsubscribe.content
+    </div>
+    <button
+      class="MuiButtonBase-root MuiButton-root MuiButton-text btnPrimary"
+      tabindex="0"
+      type="button"
+    >
+      <span
+        class="MuiButton-label"
+      >
+        unsubscribe.button_subscribe
+      </span>
+    </button>
+    <button
+      class="MuiButtonBase-root MuiButton-root MuiButton-text btnSecondary"
+      tabindex="0"
+      type="button"
+    >
+      <span
+        class="MuiButton-label"
+      >
+        unsubscribe.button_home
+      </span>
+    </button>
+  </div>
+</div>
+`;
diff --git a/src/components/Options/Unsubscribe/unSubscribeView.scss b/src/components/Options/Unsubscribe/unsubscribe.scss
similarity index 100%
rename from src/components/Options/Unsubscribe/unSubscribeView.scss
rename to src/components/Options/Unsubscribe/unsubscribe.scss
diff --git a/src/components/Routes/Routes.tsx b/src/components/Routes/Routes.tsx
index 259830e52e4c14771f18f5d2f047accdab59ebad..89d3a0590b6262642918b54382c2851aa3bf5fa6 100644
--- a/src/components/Routes/Routes.tsx
+++ b/src/components/Routes/Routes.tsx
@@ -6,7 +6,6 @@ import EcogestureFormView from 'components/EcogestureForm/EcogestureFormView'
 import EcogestureSelectionView from 'components/EcogestureSelection/EcogestureSelectionView'
 import ExplorationView from 'components/Exploration/ExplorationView'
 import Loader from 'components/Loader/Loader'
-import UnSubscribeView from 'components/Options/Unsubscribe/UnSubscribeView'
 import QuizView from 'components/Quiz/QuizView'
 import TermsView from 'components/Terms/TermsView'
 import { FluidType } from 'enums'
@@ -92,7 +91,6 @@ const AppRoutes = ({ termsStatus }: { termsStatus: TermsStatus }) => {
             <Route path="/options" element={<OptionsView />} />
             <Route path="/analysis" element={<AnalysisView />} />
             <Route path="/profiletype" element={<ProfileTypeView />} />
-            <Route path="/unsubscribe" element={<UnSubscribeView />} />
             <Route path="*" element={<Navigate replace to="/consumption" />} />
           </>
         )}
diff --git a/src/doctypes/io-cozy-permissions.ts b/src/doctypes/io-cozy-permissions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0dfde431fc863a8f5aa539d7099a636fe44f566a
--- /dev/null
+++ b/src/doctypes/io-cozy-permissions.ts
@@ -0,0 +1 @@
+export const PERMISSIONS_DOCTYPE = 'io.cozy.permissions'
diff --git a/src/locales/fr.json b/src/locales/fr.json
index ea7cc0a49987d3501a6a8f74c0cd657f4fcd6ac9..3dad86508fa2a0357d15d28b426b2e11dd938c0e 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -1287,10 +1287,12 @@
     }
   },
   "unsubscribe": {
-    "title": "Êtes-vous sûr de ne plus vouloir recevoir notre email mensuel\u00a0?",
-    "content": "Dans ce cas, vous ne recevrez plus la notification de votre bilan ainsi que les conseils associés au mois en cours.",
-    "button_text": "Oui, je me désabonne",
-    "button_accessibility": "Bouton de désinscription"
+    "error": "Une erreur est survenue lors de votre désinscription, merci de ré-essayer plus tard.",
+    "unsubscribed": "Vous êtes désormais désabonné",
+    "subscribed": "Vous allez recevoir les prochaines newsletters",
+    "content": "Vous ne recevrez plus la notification de votre bilan ainsi que les conseils associés au mois en cours.",
+    "button_subscribe": "Je souhaite me ré-abonner à la newsletter",
+    "button_home": "Me connecter"
   },
   "welcome_modal": {
     "title": "Félicitations",
diff --git a/src/services/permissions.service.ts b/src/services/permissions.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a16f3f371e597a7a496c27ae0cc7adf1b863662c
--- /dev/null
+++ b/src/services/permissions.service.ts
@@ -0,0 +1,55 @@
+import * as Sentry from '@sentry/react'
+import { Client, QueryResult } from 'cozy-client'
+import logger from 'cozy-logger'
+import { PERMISSIONS_DOCTYPE } from 'doctypes/io-cozy-permissions'
+
+const logStack = logger.namespace('challengeService')
+
+type PermissionsResponse = {
+  attributes: {
+    type: string
+    source_id: string
+    codes: {
+      code: string
+    }
+    shortcodes: {
+      code: string
+    }
+    cozyMetadata: Record<string, unknown>
+  }
+}
+
+export class PermissionsService {
+  private readonly client: Client
+
+  constructor(client: Client) {
+    this.client = client
+  }
+
+  public async getShareCode(): Promise<unknown> {
+    try {
+      const TTL = '15D'
+      const { data }: QueryResult<PermissionsResponse> = await this.client.save(
+        {
+          _type: PERMISSIONS_DOCTYPE,
+          permissions: {
+            images: {
+              type: 'com.grandlyon.ecolyo.profile',
+              verbs: ['PUT', 'GET'],
+            },
+          },
+          ttl: TTL,
+        }
+      )
+
+      return data.attributes.codes.code
+    } catch (error) {
+      const errorMessage = `Failed to create shareCode: ${JSON.stringify(
+        error
+      )}`
+      logStack('error', errorMessage)
+      Sentry.captureException(error)
+      throw error
+    }
+  }
+}
diff --git a/src/targets/browser/index.tsx b/src/targets/browser/index.tsx
index ebe12ae6d9fe158666f9c8f1546dc5f0e653c048..9e9cde5363be93509e43daa58fc5576d2d344c6a 100644
--- a/src/targets/browser/index.tsx
+++ b/src/targets/browser/index.tsx
@@ -35,9 +35,7 @@ const setupApp = memoize(() => {
   const cozyUrl = `${protocol}//${data.domain}`
 
   const locale = 'fr'
-  const polyglot: any = initTranslation(locale, lang =>
-    require(`locales/${lang}`)
-  )
+  const polyglot = initTranslation(locale, lang => require(`locales/${lang}`))
 
   const client: Client = new CozyClient({
     uri: cozyUrl,
diff --git a/src/targets/public/index.ejs b/src/targets/public/index.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..ff3f16edfb70de49a445196f21864b41c755c5c3
--- /dev/null
+++ b/src/targets/public/index.ejs
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="{{.Locale}}">
+  <head>
+    <meta charset="utf-8">
+    <title><%= htmlWebpackPlugin.options.title %> | Me désabonner</title>
+    <link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
+    <link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16">
+    <!-- PWA Manifest -->
+    <link rel="manifest" href="/manifest.json" crossOrigin="use-credentials">
+    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#297EF2">
+    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, viewport-fit=cover">
+    <!-- PWA Chrome -->
+    <link rel="icon" sizes="192x192" href="/android-chrome-192x192.png">
+    <link rel="icon" sizes="512x512" href="/android-chrome-512x512.png">
+    <!-- PWA iOS -->
+    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
+    <link rel="apple-touch-startup-image" href="/apple-touch-icon.png">
+    <meta name="apple-mobile-web-app-title" content="Ecolyo">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-status-bar-style" content="black">
+    <!-- PWA Colors -->
+    <meta name="theme-color" content="#343641" />
+    <meta name="background-color" content="#121212" />
+
+    <% _.forEach(htmlWebpackPlugin.files.css, function(file) { %>
+        <link rel="stylesheet" href="<%- file %>">
+    <% }); %>
+    {{.ThemeCSS}}
+
+    <% if (__TARGET__ === 'mobile') { %>
+    <meta name="format-detection" content="telephone=no">
+    <script src="cordova.js" defer></script>
+    <% } else if (__STACK_ASSETS__) { %>
+    {{.CozyBar}}
+    <% } %>
+    <script src="//{{.Domain}}/assets/js/piwik.js"></script>
+
+  </head>
+  <body>
+  <div
+    role="application"
+    class="application"
+    data-cozy="{{.CozyData}}"
+  >
+  <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
+      <script src="<%- file %>"></script>
+  <% }); %>
+  </body>
+</html>
\ No newline at end of file
diff --git a/src/targets/public/index.tsx b/src/targets/public/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..294a37fb174f66b2c2747e92b9ef5c22ec51a1f2
--- /dev/null
+++ b/src/targets/public/index.tsx
@@ -0,0 +1,119 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-var-requires */
+declare let __SENTRY_DSN__: string
+
+import * as Sentry from '@sentry/react'
+import { BrowserTracing } from '@sentry/tracing'
+import Unsubscribe from 'components/Options/Unsubscribe/Unsubscribe'
+import CozyClient, { Client, CozyProvider } from 'cozy-client'
+import { isFlagshipApp } from 'cozy-device-helper'
+import { WebviewIntentProvider } from 'cozy-intent'
+import { I18n, initTranslation } from 'cozy-ui/transpiled/react/I18n'
+import schema from 'doctypes'
+import { memoize } from 'lodash'
+import React from 'react'
+import { createRoot } from 'react-dom/client'
+import EnvironmentService from 'services/environment.service'
+import cozyBar from 'utils/cozyBar'
+import logApp from 'utils/logger'
+import manifest from '../../../manifest.webapp'
+import '../../styles/index.scss'
+
+const getToken = (): string => {
+  const search = new URLSearchParams(window.location.search)
+  return search.get('token') || ''
+}
+
+const setupApp = memoize(() => {
+  const publicToken = getToken()
+  const container: any = document.querySelector('[role=application]')
+  const data = JSON.parse(container.dataset.cozy)
+  const protocol = window.location.protocol
+  const cozyUrl = `${protocol}//${data.domain}`
+
+  const locale = 'fr'
+  const polyglot = initTranslation(locale, lang => require(`locales/${lang}`))
+
+  const client: Client = new CozyClient({
+    uri: cozyUrl,
+    token: publicToken,
+    appMetadata: {
+      slug: manifest.name,
+      version: manifest.version,
+    },
+    schema,
+  })
+
+  const envService = new EnvironmentService()
+  const isLocal = envService.isLocal()
+  const development = envService.isDev()
+
+  cozyBar.init({
+    appName: data.app.name,
+    appEditor: data.app.editor,
+    cozyClient: client,
+    iconPath: data.app.icon,
+    lang: data.locale,
+    replaceTitleOnMobile: false,
+    appSlug: data.app.slug,
+    appNamePrefix: data.app.prefix,
+    isInvertedTheme: isFlagshipApp(),
+  })
+
+  !isLocal &&
+    Sentry.init({
+      dsn: __SENTRY_DSN__,
+      integrations: [new BrowserTracing()],
+      // Set tracesSampleRate to 1.0 to capture 100%
+      // of transactions for performance monitoring.
+      // We recommend adjusting this value in production
+      // Set to 0 for local development
+      tracesSampleRate: 1.0,
+
+      // Custom settings below
+      release: client.appMetadata.version,
+      environment: development ? 'development' : 'production',
+      // cast because init is somehow missing dsn property
+    } as Sentry.BrowserOptions)
+
+  return { container, client, locale, polyglot }
+})
+
+const init = () => {
+  const { container, client, locale, polyglot } = setupApp()
+  const root = createRoot(container)
+
+  root.render(
+    <WebviewIntentProvider setBarContext={cozyBar.setWebviewContext}>
+      <CozyProvider client={client}>
+        <I18n lang={locale} polyglot={polyglot}>
+          <Unsubscribe />
+        </I18n>
+      </CozyProvider>
+    </WebviewIntentProvider>
+  )
+}
+
+// initial rendering of the application
+document.addEventListener('DOMContentLoaded', () => init())
+// excludes Chrome, Edge, and all Android browsers that include the Safari name in their user agent
+const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
+if (!isSafari && 'serviceWorker' in navigator) {
+  window.addEventListener('load', function () {
+    navigator.serviceWorker
+      .register('/serviceWorker.js')
+      .then(reg => console.log('service worker registered', reg.scope))
+      .catch(error => {
+        const errorMessage = `service worker not registered: ${JSON.stringify(
+          error
+        )}`
+        logApp.error(errorMessage)
+        Sentry.captureException(errorMessage)
+      })
+  })
+}
+
+if (module.hot) {
+  init()
+  module.hot.accept()
+}
diff --git a/src/targets/services/monthlyReportNotification.ts b/src/targets/services/monthlyReportNotification.ts
index 9d4dc2fd2360e451764cffcb5dca51d7e8a6fb95..276c47cac6d62fac31d5fe143bb8d346c543b493 100644
--- a/src/targets/services/monthlyReportNotification.ts
+++ b/src/targets/services/monthlyReportNotification.ts
@@ -10,6 +10,7 @@ import { MonthlyReport, PerformanceIndicator } from 'models'
 import ConsumptionService from 'services/consumption.service'
 import EnvironmentService from 'services/environment.service'
 import MailService from 'services/mail.service'
+import { PermissionsService } from 'services/permissions.service'
 import ProfileService from 'services/profile.service'
 import { getMonthNameWithPrep } from 'utils/utils'
 import { runService } from './service'
@@ -155,9 +156,10 @@ const monthlyReportNotification = async ({
   client,
 }: MonthlyReportNotificationProps) => {
   logStack('info', 'Fetching user profile...')
-  const upm = new ProfileService(client)
-  let userProfil = await upm.getProfile()
-  if (!userProfil?.sendAnalysisNotification) {
+  const profileService = new ProfileService(client)
+  const permissionsService = new PermissionsService(client)
+  const userProfile = await profileService.getProfile()
+  if (!userProfile?.sendAnalysisNotification) {
     logStack(
       'info',
       'End of process - Report Notification disabled in user profile'
@@ -166,11 +168,11 @@ const monthlyReportNotification = async ({
   }
 
   // Init mail token for user in case he don't have one
-  if (!userProfil.mailToken || userProfil.mailToken === '') {
+  if (!userProfile.mailToken || userProfile.mailToken === '') {
     const token: string = require('crypto').randomBytes(48).toString('hex')
 
     try {
-      await upm.updateProfile({
+      await profileService.updateProfile({
         mailToken: token,
       })
     } catch (error) {
@@ -181,7 +183,7 @@ const monthlyReportNotification = async ({
   }
 
   let username = ''
-  let url = ''
+  let analysisLink = ''
 
   logStack('info', 'Fetching data for mail...')
   // Retrieve public name from the stack
@@ -197,7 +199,7 @@ const monthlyReportNotification = async ({
   const apps = await client.getStackClient().fetchJSON('GET', '/apps/ecolyo')
   const appLink = get(apps, 'data.links.related')
   if (appLink) {
-    url = appLink
+    analysisLink = appLink
   }
 
   logStack('info', 'Creation of mail...')
@@ -208,39 +210,34 @@ const monthlyReportNotification = async ({
   })
   const month = today.toFormat('MM')
   const year = today.toFormat('yyyy')
-  const monthlyReport: MonthlyReport = await getMonthlyReport(
-    year,
-    month,
-    client
-  )
+  const monthlyReport = await getMonthlyReport(year, month, client)
 
-  let unsubscribeUrl
-  userProfil = await upm.getProfile()
-  let token = undefined
-  if (userProfil?.mailToken) {
-    token = userProfil.mailToken
+  /** bounce token, will be used to calculate bounce clicks from monthly report */
+  let bounceToken = undefined
+  if (userProfile?.mailToken) {
+    bounceToken = userProfile.mailToken
   }
 
-  if (!url.includes('analysis')) {
-    unsubscribeUrl = url + '/#/unsubscribe'
-    url = url + '/#/analysis'
-    if (token) {
-      url += '?token=' + token
-    }
-  } else {
-    unsubscribeUrl = url.replace('analysis', 'unsubscribe')
+  if (!analysisLink.includes('analysis')) {
+    analysisLink = analysisLink + '/#/analysis'
+  }
+  if (bounceToken) {
+    analysisLink += '?token=' + bounceToken
+  }
+
+  let unsubscribeUrl = appLink + 'public'
+  const unsubscribeToken = await permissionsService.getShareCode()
+  if (unsubscribeToken) {
+    unsubscribeUrl += `?token=${unsubscribeToken}`
   }
 
   const monthComparisonText = await buildComparisonText(client, 'month')
   const yearComparisonText = await buildComparisonText(client, 'year')
 
-  const isInfo: boolean = monthlyReport.info !== ''
-
-  const isServiceNews: boolean =
+  const isInfo = monthlyReport.info !== ''
+  const isServiceNews =
     monthlyReport.newsTitle !== '' && monthlyReport.newsContent !== ''
-
-  const isPoll: boolean =
-    monthlyReport.question !== '' && monthlyReport.link !== ''
+  const isPoll = monthlyReport.question !== '' && monthlyReport.link !== ''
 
   const date = DateTime.local()
     .setZone('utc', { keepLocalTime: true })
@@ -253,7 +250,7 @@ const monthlyReportNotification = async ({
     title: 'Infos & bilan consos',
     baseUrl: baseUrl,
     username: username,
-    clientUrl: url,
+    clientUrl: analysisLink,
     unsubscribeUrl: unsubscribeUrl,
     comparisonExist:
       monthComparisonText.length > 0 || yearComparisonText.length > 0,
diff --git a/src/targets/vendor/assets/offline.html b/src/targets/vendor/assets/offline.html
index f310b42c32a6ccea791d848a0deaf5c61340cbcc..eff28e28a50078ea94b7939c7a5f893ada385681 100644
--- a/src/targets/vendor/assets/offline.html
+++ b/src/targets/vendor/assets/offline.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
     <meta charset="UTF-8" />
@@ -52,30 +52,7 @@
     <nav></nav>
     <div class="container">
       <section>
-        <svg
-          width="80"
-          height="80"
-          viewBox="0 0 16 16"
-          fill="none"
-          xmlns="http://www.w3.org/2000/svg"
-        >
-          <path
-            d="M8.01323 15.059C14.8544 12.3888 14.3227 8.59276 13.9682 2.93081C11.7705 2.8343 9.82099 2.15873 8.01323 1.03278C6.20546 2.15873 4.25592 2.8343 2.05825 2.93081C1.70378 8.59276 1.17209 12.3888 8.01323 15.059Z"
-            fill="#1B1C22"
-          />
-          <path
-            d="M8.01325 0L7.54979 0.288665C5.85495 1.34429 4.05006 1.96586 2.01981 2.05502L1.23259 2.08959L1.18335 2.87604C1.16873 3.10958 1.15353 3.34193 1.13842 3.57294C0.97583 6.05878 0.823332 8.39029 1.50758 10.3994C2.29645 12.7158 4.12651 14.483 7.69452 15.8756L8.01325 16V15.059C1.44034 12.4935 1.67345 8.88869 2.01622 3.58822C2.03021 3.37192 2.04438 3.1528 2.05827 2.93081C4.25595 2.8343 6.20549 2.15873 8.01325 1.03277V0Z"
-            fill="#FFC600"
-          />
-          <path
-            d="M8.01323 0L8.4767 0.288665C10.1715 1.34429 11.9764 1.96586 14.0067 2.05502L14.7939 2.08959L14.8431 2.87604C14.8578 3.10958 14.873 3.34193 14.8881 3.57294C15.0507 6.05878 15.2032 8.39029 14.5189 10.3994C13.73 12.7158 11.9 14.483 8.33197 15.8756L8.01323 16V15.059C14.5862 12.4935 14.353 8.88869 14.0103 3.58822C13.9963 3.37192 13.9821 3.1528 13.9682 2.93081C11.7705 2.8343 9.821 2.15873 8.01323 1.03277V0Z"
-            fill="#DB8300"
-          />
-          <path
-            d="M5.85148 5.51416H6.16248C6.28668 5.51416 6.4058 5.56292 6.49363 5.64973C6.58146 5.73653 6.6308 5.85427 6.6308 5.97703V10.6758H5.38316V5.97703C5.38316 5.85427 5.4325 5.73653 5.52033 5.64973C5.60815 5.56292 5.72727 5.51416 5.85148 5.51416ZM8.01233 7.80214H8.32333C8.44754 7.80214 8.56666 7.85091 8.65448 7.93771C8.74231 8.02452 8.79165 8.14225 8.79165 8.26502V10.6758H7.54401V8.26502C7.54401 8.14225 7.59335 8.02452 7.68118 7.93771C7.76901 7.85091 7.88813 7.80214 8.01233 7.80214ZM10.302 6.91708H10.613C10.7372 6.91708 10.8563 6.96584 10.9441 7.05265C11.032 7.13945 11.0813 7.25719 11.0813 7.37995V10.6758H9.83366V7.37995C9.83366 7.25719 9.883 7.13945 9.97083 7.05265C10.0587 6.96584 10.1778 6.91708 10.302 6.91708Z"
-            fill="#FFC600"
-          />
-        </svg>
+        <img src="./icon.svg" alt="logo Ecolyo" height="80px" width="80px" />
         <p class="text-16-white">Hors ligne</p>
         <p class="text-16-white">
           Vérifiez votre connexion pour lancer Ecolyo.
diff --git a/src/targets/vendor/assets/unsubscribe.html b/src/targets/vendor/assets/unsubscribe.html
new file mode 100644
index 0000000000000000000000000000000000000000..eff28e28a50078ea94b7939c7a5f893ada385681
--- /dev/null
+++ b/src/targets/vendor/assets/unsubscribe.html
@@ -0,0 +1,64 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="description"
+      content="Ecolyo est le service proposé par la Métropole de Lyon pour suivre et comprendre la consommation énergétique globale de votre foyer."
+    />
+    <meta
+      http-equiv="Content-Security-Policy"
+      content="style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;"
+    />
+    <link rel="stylesheet" href="./style.css" />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&display=swap"
+      rel="stylesheet"
+    />
+    <link rel="icon" href="assets/icon.svg" />
+    <title>Ecolyo</title>
+    <style type="text/css">
+      * {
+        margin: 0;
+        line-height: 1;
+        font-family: 'Lato', sans-serif;
+        color: white;
+      }
+      html,
+      body {
+        height: 100%;
+        margin: auto;
+        background: #121212;
+      }
+      p {
+        margin-top: 1rem;
+      }
+      .container {
+        height: 100%;
+        display: flex;
+        align-items: center;
+        text-align: center;
+        justify-content: center;
+      }
+      .text-16-white {
+        font-weight: 900;
+        font-size: 1rem;
+        color: white;
+      }
+    </style>
+  </head>
+  <body>
+    <nav></nav>
+    <div class="container">
+      <section>
+        <img src="./icon.svg" alt="logo Ecolyo" height="80px" width="80px" />
+        <p class="text-16-white">Hors ligne</p>
+        <p class="text-16-white">
+          Vérifiez votre connexion pour lancer Ecolyo.
+        </p>
+      </section>
+    </div>
+    <footer></footer>
+  </body>
+</html>