Vous avez déployé un hotfix vendredi. Lundi, le support reçoit encore des retours d'utilisateurs qui ne l'ont jamais reçu, les bêta-testeurs sont bloqués sur un bundle obsolète, et un client entreprise veut savoir exactement quelle version utilise son équipe sur le terrain. C'est là qu'on comprend qu'une notification de mise à jour d'app ce n'est pas une modale. C'est un système d'exploitation pour le contrôle des releases.
Dans les projets Capacitor et Electron, la difficulté n'est généralement pas de détecter qu'une mise à jour existe. La difficulté réside dans tout ce qui l'entoure : décider qui doit la voir, quand, que faire s'ils l'ignorent, comment la mise à jour circule via la CI/CD, et ce que la télémétrie vous indique après le déploiement. Si vous traitez les prompts de mise à jour comme un simple élément d'UI, vous obtenez des notifications intrusives, une logique de release fragile et des utilisateurs confus. Si vous les traitez comme une partie du cycle de vie du produit, vous obtenez des déploiements plus sûrs et un support beaucoup plus serein.
Table of Contents
- Pourquoi votre stratégie de mise à jour d'app compte
- Implémenter la détection de mise à jour avec Capgo
- Concevoir des patterns de notification efficaces
- Automating Update Flows and User Choice
- Déploiements progressifs avancés avec canaux et télémétrie
- Résolution des problèmes de notification courants
Pourquoi votre stratégie de mise à jour d'app compte
Mises à jour : la rétention, pas seulement la maintenance
Les équipes voient souvent les mises à jour comme une corvée de maintenance. Corriger le bug, envoyer un prompt, et passer à autre chose. Cette mentalité ignore l'impact sur le produit.
Les notifications push sont l'un des rares canaux de cycle de vie capables de ramener les utilisateurs dans l'app après l'installation. Les données résumées par les recherches d'Invesp sur les notifications push mobiles indique que les notifications push peuvent booster l'engagement de l'app de jusqu'à 88 %, et les utilisateurs ayant accepté sont conservés à presque 2x plus the rate of users who don’t. For update strategy, that matters because every stale client is a user who may never see the feature, fix, or compliance change you just shipped.
Un flux de mise à jour médiocre crée généralement trois problèmes simultanément :
- Le retard produit means new features launch unevenly, so PMs read mixed signals from analytics.
- Support par glisser-déposer apparaît lorsque les agents doivent demander des captures d'écran, des versions et des détails sur l'appareil avant même de pouvoir reproduire un problème.
- Exposition de la sécurité augmente lorsque d'anciens clients continuent de communiquer avec des API qui ont déjà évolué.
Règle pratique : considérez la livraison des mises à jour comme une partie de la gestion des releases, et non comme un simple message de courtoisie en fin de sprint.
Mises à jour stores et live updates : des enjeux différents
Les mises à jour de l'App Store et du Play Store restent essentielles. Les changements de dépendances natives, les releases liées aux politiques, les changements de permissions et les correctifs au niveau binaire relèvent de ce domaine. Mais les mises à jour via les stores ne sont qu'une couche du système, et elles sont lentes par conception car la revue et l'adoption par les utilisateurs échappent à votre contrôle direct.
Pour les applications Capacitor et Electron, les live updates couvrent une catégorie de travail différente. Ils sont adaptés aux changements de bundles web tels que le JavaScript, le CSS, le texte, les assets et les feature flags qui ne nécessitent pas de nouveau binaire. En pratique, cela vous permet de séparer deux questions de release :
| Question de release | Meilleure option |
|---|---|
| Ce changement nécessite-t-il un nouveau binaire natif ? | Release sur les stores |
| Ce changement peut-il être envoyé via un bundle web en toute sécurité ? | Live update |
| Do users need to know before continuing? | Décision de notification in-app |
| Seuls certains utilisateurs en ont-ils besoin maintenant ? | Déploiement progressif par canal |
That split is why agencies building client apps should stop designing around a single “update available” pop-up. Professional teams need soft prompts, silent apply paths, rollback rules, channel targeting, and logs that support can inspect later.
La question de la confiance est également cruciale. Les utilisateurs ne craignent pas les mises à jour autant que les interruptions imprévisibles. Si l'app se met à jour de manière fluide, explique clairement les changements majeurs et ne bloque l'utilisation qu'en cas de bug critique ou de risque de sécurité, cela est perçu comme un signe de compétence.
Implémenter la détection de mise à jour avec Capgo
La première étape est simple : savoir quelle version l'utilisateur utilise, à quel canal il appartient, et décider s'il y a quelque chose à récupérer. La plupart des systèmes de mise à jour DIY deviennent complexes car ils mélangent ces décisions. Gardez-les séparées.

Commencez par identifier la version
Un updater fiable nécessite trois valeurs disponibles au runtime :
- Version de l'app installée
- Canal de release assigné
- État actuel de la mise à jour, comme idle, checking, available, downloading, ready, failed
Si vous ignorez ce modèle d'état, des bugs de notification apparaissent vite. L'app vérifie trop souvent. Le même prompt s'affiche à chaque lancement. Un téléchargement en arrière-plan se termine, mais l'UI indique toujours « checking ».
Un service managé est généralement la meilleure option pour une raison : la charge opérationnelle est plus lourde que ce que suggère le snippet code. Vous avez besoin de bundles signés, de règles de canaux, du support de rollback, de l'historique des versions, des logs par appareil et d'une infrastructure de livraison. Capgo fournit cela pour Capacitor et les apps Electron via un plugin d'updater et un workflow de livraison hébergé, c'est pourquoi la plupart des équipes préfèrent l'utiliser plutôt que de reconstruire toute la stack en interne.
Branchez l'updater au démarrage de l'app
Au lancement de l'app, effectuez une vérification légère une fois votre shell prêt. Ne bloquez pas le premier rendu (first paint) sauf si l'app ne peut pas continuer sans la mise à jour.
Un pattern typique dans une application Capacitor ressemble à ceci :
import { App } from '@capacitor/app'
// import your updater SDK here
type UpdateDecision =
| { kind: 'none' }
| { kind: 'soft'; version: string }
| { kind: 'hard'; version: string }
| { kind: 'silent'; version: string }
async function checkForUpdate(): Promise<UpdateDecision> {
try {
// Replace with your updater SDK call
const result = await updater.check()
if (!result || !result.available) {
return { kind: 'none' }
}
if (result.metadata?.mandatory === true) {
return { kind: 'hard', version: result.version }
}
if (result.metadata?.silent === true) {
return { kind: 'silent', version: result.version }
}
return { kind: 'soft', version: result.version }
} catch {
return { kind: 'none' }
}
}
App.addListener('appStateChange', async ({ isActive }) => {
if (!isActive) return
const decision = await checkForUpdate()
handleUpdateDecision(decision)
})
L'objectif de check() isn’t just “is there a newer thing”. It’s “is there a newer thing for ce user on ce canal, et comment l'application doit réagir.
Une implémentation saine enregistre également l'heure du dernier check réussi et la dernière version proposée. Cela permet de rendre votre logique de notification de mise à jour idempotente plutôt qu'intrusive.
Lisez le résultat et branchez tôt
La branche doit être créée le plus près possible du résultat du check. Ne dispersez pas les règles de mise à jour sur plusieurs écrans.
Voici la séparation pratique que j'utilise :
- No update means do nothing and log a normal check result.
- Mise à jour douce signifie mettre en file d'attente une bannière, un badge de paramètres ou un prompt léger in-app.
- Mise à jour silencieuse signifie un téléchargement en arrière-plan avec activation au prochain lancement.
- Hard update signifie de basculer l'app vers un flux de blocage contrôlé.
Plus tard dans l'implémentation, j'aime exposer cette décision via un store central pour que React, Vue ou l'UI Ionic puissent la consommer de manière cohérente.
Ce guide est utile si vous souhaitez voir la configuration globale autour d'une app Capacitor :
Gardez la couche de détection simple. L'intelligence doit résider dans la politique de rollout, pas dans le code au démarrage.
Concevoir des patterns de notification efficaces
La plupart des prompts de mise à jour échouent parce que l'équipe a choisi un seul pattern pour tout. C'est ainsi que l'on finit par afficher une modale bloquante pour une simple correction de texte, ou par cacher une migration critique derrière un toast que personne ne remarque.
L'environnement est déjà saturé. Le résumé du benchmark Airship de Business of Apps indique que l'utilisateur moyen de smartphone aux États-Unis reçoit 46 notifications push par jour, alors que les taux moyens de réaction et de clic restent modestes à 3,4 % sur iOS et 4,6 % sur Android. Une notification de mise à jour d'app doit capter l'attention sans épuiser l'utilisateur.

Utilisez le pattern le moins intrusif qui fonctionne
Une bonne interface de mise à jour respecte le coût de l'interruption. Si l'utilisateur saisit des coordonnées de paiement, note un dossier patient ou scanne un inventaire, une modale peut être pire que le bug que vous tentez de corriger.
D'habitude, je structure les patterns ainsi :
- Bannière haute ou basse pour les correctifs mineurs, les améliorations peu urgentes et la confirmation de mise à jour silencieuse.
- Toast pour l'état en arrière-plan, comme « Mise à jour prête au prochain lancement », mais pas pour des décisions critiques.
- Point d'entrée dans les paramètres ou le profil pour les utilisateurs qui veulent garder le contrôl’et voir le changelog.
- Modale bloquante only when the app can’t safely continue on the old version.
Une bannière subtile est souvent plus efficace qu'une modale intrusive, car elle n'oblige pas l'utilisateur à lutter contre l'interface.
Comparatif rapide des principaux patterns
| Pattern | Points positifs | Risque principal | Note d'implémentation |
|---|---|---|---|
| Bannière | Mises à jour optionnelles, rappels peu urgents | Facile à ignorer | Persister l'ignorance par version |
| Toast | Changements d'état en arrière-plan | Disparaît trop vite | Coupler avec une entrée de configuration durable |
| Message in-app | Déploiements progressifs de fonctionnalités | Peuvent ne pas être vus rapidement | Liez-l’à un écran pertinent |
| Modale | Action obligatoire | Frustration de l'utilisateur | À réserver uniquement aux étapes critiques |
Le détail d'implémentation le plus important est la persistance de l'état. Si un utilisateur appuie sur « Plus tard », enregistrez cette préférence pour la version proposée. S'il ferme une bannière, ne la réaffichez pas à chaque changement de route. Si vous oubliez cela, les utilisateurs percevront l'app comme étant buggée, même si l'updater fonctionne.
Pour les équipes utilisant déjà les notifications push dans leur stack, il est utile de comparer l'UX de mise à jour de l'app avec votre système de messagerie global. Le guide de Capgo sur Ionic et les notifications push Capacitor avec Firebase est utile ici car elle permet de séparer les problématiques de transport des interfaces in-app qui sollicitent l'utilisateur.
Le push n'est qu'une partie de l'équation
Une erreur courante consiste à penser que les badges de mise à jour de l'OS et les notifications des stores suffiront. En réalité, les utilisateurs ignorent souvent ces alertes à cause des paramètres de l'appareil, des permissions de badge, du comportement de mise à jour automatique ou des modes d'économie d'énergie. C'est pourquoi la messagerie in-app reste essentielle, même quand l'écosystème du store fonctionne correctement.
Pour Electron, c'est encore plus flagrant. Les utilisateurs de bureau attendent souvent des indicateurs de statut discrets, pas des interruptions modales. Un petit badge « Mise à jour prête » dans le shell peut paraître plus professionnel qu'une boîte de dialogue système qui vole le focus en plein milieu d'un workflow.
Le meilleur pattern est celui qui correspond au risque de la mise à jour et à la tâche actuelle de l'utilisateur. Tout le reste n'est que de la mise en scène.
Automating Update Flows and User Choice
Une fois que la détection et les patterns UX sont en place, le cœur du système est le workflow. Dans ce cadre, les équipes ont tendance soit à trop automatiser et à perdre le contrôle, soit à ne pas assez automatiser et à créer une dette de support.

Coderio’s app maintenance guidance recommandent un rythme de release pratique de mises à jour mineures toutes les 2 à 4 semaines et des releases majeures tous les 3 à 6 mois, avec des mises à jour natives réservées aux problèmes critiques de sécurité ou de stabilité. C'est le bon modèle mental. La décision doit dépendre du type de release, pas de l'anxiété du développeur.
Silent updates for low-risk changes
Les mises à jour silencieuses sont le levier le moins utilisé dans les apps Capacitor. Si vous avez corrigé du style, du texte, du wiring de feature-flags ou un bug JavaScript non bloquant, il n'y a généralement aucune raison d'interrompre l'utilisateur.
Le flux est simple :
- L'app vérifie si un nouveau bundle est disponible.
- Si la mise à jour est marquée comme sûre pour une application en arrière-plan, elle est téléchargée en arrière-plan.
- L'app active le nouveau bundle au prochain lancement.
- L'utilisateur peut voir une brève note « Mise à jour réussie » après le redémarrage, ou rien du tout.
Ce dernier choix dépend du changement. Si la mise à jour a modifié un workflow visible, une petite carte « Quoi de neuf » au prochain lancement aide à l'orientation. Sinon, la discrétion est préférable.
Un gestionnaire d'état simple peut ressembler à ceci :
async function handleUpdateDecision(decision: UpdateDecision) {
if (decision.kind === 'silent') {
await updater.download()
await updater.setNextBundle()
localStorage.setItem('pendingUpdateVersion', decision.version)
return
}
if (decision.kind === 'soft') {
showBanner(decision.version)
return
}
if (decision.kind === 'hard') {
showForcedUpdateScreen(decision.version)
}
}
Flux de choix utilisateur pour les changements de produit visibles
Un flux de choix utilisateur est adapté quand la mise à jour modifie suffisamment le comportement pour que l'utilisateur doive accepter l'interruption. Une nouvelle navigation, un onboarding révisé, un flux d'approbation modifié ou un redesign majeur du dashboard entrent tous dans cette catégorie.
Le prompt doit rester concis :
- Ce qui a changé
- Pourquoi c'est important
- Ce qui se passe s'ils mettent à jour maintenant
- Ce qui se passe s'ils attendent
N'écrivez pas de la poésie de release notes dans la boîte de dialogue. Une phrase claire et deux boutons sont généralement plus efficaces qu'un mur de texte.
J'aime ce pattern :
Nouvelle version disponible. Elle inclut le nouveau workflow de reporting et corrige un problème d'export. Mettre à jour maintenant ou continuer et installer plus tard.
Utilisez « Plus tard » avec discernement. Si l'ancien client reste valide, laissez l'utilisateur continuer. Si l'ancien client va casser à cause d'une migration API, ne faites pas semblant que c'est optionnel.
Pour les équipes qui réfléchissent à la gouvernance au-delà de la simple livraison d'apps, la même logique s'applique aux opérations de sécurité. Une bonne automatisation gère les changements de routine en toute discrétion et ne déclenche une alerte que lorsque le risque le justifie. C'est l'une des raisons pour lesquelles cet aperçu de l' security automation for SOC teams est utile. Cela illustre un principe de conception plus large : classifier les événements, automatiser les parcours sécurisés et rendre l'intervention humaine intentionnelle.
Vous pouvez également affiner cela grâce à une logique d'audience. L'article de Capgo sur segmentation par fréquence d'usage pour les mises à jour d'app est une référence pratique, car les utilisateurs fréquents et occasionnels ne devraient pas toujours recevoir le même timing ou le même style de prompt.
Mises à jour forcées pour les cas critiques restreints
Les mises à jour forcées sont légitimes. Elles sont aussi faciles à détourner.
Utilisez un hard gate si l'une de ces conditions est remplie :
| Condition | Forcer la mise à jour |
|---|---|
| Correctif de sécurité avec exposition connue | Oui |
| Problème de stabilité causant une rupture majeure | Oui |
| Rupture du contrat backend | Oui |
| Ajustements mineurs de l'UI | Non |
| Déploiement progressif d'une fonctionnalité optionnelle | Non |
L'implémentation doit être explicite. Vérifiez la version installée au lancement, comparez-l’à votre version minimale supportée, et bloquez l'utilisateur uniquement s'il tombe sous ce seuil. Ne déduisez pas qu'une mise à jour est « obligatoire » simplement parce qu'une version plus récente existe.
Un écran de mise à jour forcée nécessite trois propriétés :
- Pas d'impasseOffrez à l'utilisateur une option de réessai claire.
- Explication claire. Expliquez-leur pourquoi la mise à jour est nécessaire.
- Gestion du mode hors ligne. Si le réseau est indisponible, expliquez-l’également.
Ce qui ne fonctionne pas, c'est une modale avec un seul bouton « Mettre à jour » qui échoue sans indication sur une connexion mobile instable. Si l'app est bloquée, le parcours de récupération doit être plus soigné que le parcours classique.
Déploiements avancés avec canaux et télémétrie
La plupart des incidents de mise à jour ne surviennent pas à cause d'un échec de détection. Ils arrivent parce que l'équipe a déployé massivement avant de comprendre l'impact réel de la mise à jour sur le terrain.
Les canaux réduisent le rayon d'impact
Le déploiement par canaux est la méthode la plus sûre pour livrer des live updates dans les apps clientes. Au lieu de publier un bundle pour tout le monde, publiez vers des audiences spécifiques : interne, QA, bêta, staging, production, ou même des flux dédiés à certains clients.
Cela vous offre une structure de release qui ressemble davantage à un contrôl’opérationnel qu'à un lancement binaire. Un build peut traverser une séquence d'audiences, chaque groupe vous apportant la confiance nécessaire avant de passer au suivant.
Voici une capture d'écran utile illustrant l'aspect commercial de ce modèle de déploiement, incluant la structure des plans autour des workflows de mise à jour.

Cela impacte également la stratégie de notification. Les bonnes pratiques de notifications push d'Adapty indiquent que l'optimisation des heures d'envoi peut augmenter les taux de réaction de 40 % et un ciblage avancé peut tripler les taux de réaction. Dans les systèmes de mise à jour, cela se traduit par un déploiement progressif par canal et des messages spécifiques à chaque version, plutôt que des prompts génériques envoyés à toute la base installée.
La télémétrie vous indique si les utilisateurs ont réellement migré
Un système de mise à jour professionnel doit répondre à ces questions sans que l'ingénierie ait à fouiller dans des logs ad hoc :
- Sur quelle version de bundle se trouve chaque appareil ?
- La mise à jour a-t-elle été téléchargée ?
- L'application a-t-elle réussi au prochain lancement ?
- Les échecs au démarrage ont-ils augmenté après le déploiement ?
- Quels utilisateurs sont bloqués sur une version obsolète ?
C'est là que la télémétrie transforme l'acte de release en un processus opérationnel. Sans elle, vous savez seulement ce que vous avez déployé. Avec elle, vous savez ce que les utilisateurs ont adopté.
Si le support ne peut pas voir l'état de la mise à jour, il escaladera un problème produit qui est en réalité un problème de déploiement.
Je préfère largement les chronologies par appareil aux dashboards uniquement agrégés. Les courbes d'adoption agrégées sont utiles, mais elles n'expliqueront pas pourquoi un client entreprise utilise encore l'app avec un ancien bundle après une semaine. Les logs au niveau de l'appareil, si.
La publication ciblant une version devient aussi plus pratique quand on peut isoler des cohortes spécifiques. Ce guide sur l'envoi d'une version spécifique aux utilisateurs est un bon exemple du type de contrôle dont les équipes enterprise finissent généralement par avoir besoin lorsqu'elles gèrent plusieurs environnements clients.
Le CI/CD doit publier et observer, pas seulement build
Un pipeline moderne ne devrait pas s'arrêter à « build réussi ». Il devrait :
- Compiler le bundle
- Signez et publiez sur le bon canal
- Ajoutez les métadonnées de la release
- Surveillez l'adoption et les échecs
- Effectuez un rollback si la santé décline
Le rollback est ce qui distingue un système de mise à jour de démo d'un système de production. Si un bundle provoque des crashs au lancement ou des deadlocks au démarrage, les équipes doivent pouvoir limiter la zone d'impact rapidement. C'est l'une des raisons majeures pour lesquelles les outils managés surpassent le DIY pour la plupart des agences. La livraison, les garde-fous, l'observabilité et le rollback ne sont pas des fonctionnalités secondaires. Ils constituent le système.
L'intégration CI/CD en elle-même n'a pas besoin d'être complexe. Ce qui compte, c'est que la publication soit déterministe et traçable. Une release doit pouvoir être attribuée à un commit, un environnement, un acteur et un canal. Si vous ne pouvez pas répondre rapidement à ces quatre points, la gestion d'incident devient ingérable.
Dépanner les problèmes de notification courants
Les problèmes ci-dessous apparaissent régulièrement dans Capacitor et les mises à jour Electron. La plupart d'entre eux proviennent d'une dérive d'état, et non du réseau.
Le prompt apparaît à chaque lancement
Symptôme : les utilisateurs ignorent la notification de mise à jour de l'app, mais elle réapparaît à chaque ouverture de l'app.
Cause probable : la vérification réussit, mais l'état du prompt n'est pas persisté par version proposée.
Correction : stockez la version que l'utilisateur a ignorée ou reportée, et comparez-l’avant d'afficher à nouveau l'UI.
function shouldPrompt(version: string): boolean {
const dismissed = localStorage.getItem('dismissedUpdateVersion')
return dismissed !== version
}
function dismissPrompt(version: string) {
localStorage.setItem('dismissedUpdateVersion', version)
}
C'est aussi là que les équipes confondent « disponible » et « doit interrompre ». Ce sont deux décisions distinctes.
Silent updates download but never activate
Symptôme : les logs montrent qu'un bundle a été récupéré, mais l'ancienne UI continue de charger.
Cause probable : l'app a téléchargé la mise à jour mais ne l'a jamais marquée pour le prochain lancement, ou votre chemin de démarrage pointe toujours vers le dernier bundle actif.
Correction : rendez l'activation explicite et vérifiez-l’au démarrage. Traitez « téléchargé » et « actif » comme des états distincts dans code et vos analytics.
Beaucoup de bugs disparaissent quand vous modélisez le cycle de vie comme available -> downloading -> ready -> active au lieu d'un seul booléen.
Le comportement des vérifications diffère entre le dev et la prod
Symptôme : la détection de mise à jour fonctionne sur un build de release mais pas en développement local, ou inversement.
Cause probable : configuration spécifique à l'environnement. Des noms de canaux différents, des plugins désactivés en debug, ou un démarrage code enveloppé dans le mauvais guard.
Solution : rendez le comportement de l'environnement visible. Loggez le canal, la version de l'app et le mode de build au démarrage. Ne vous fiez pas à votre mémoire.
- Les builds de développement devraient généralement ignorer les vérifications live update ou pointer vers un canal de test dédié.
- Les builds de staging devraient se comporter comme la prod, mais sur des flux de déploiement progressif isolés.
- Les builds de production ne doivent jamais partager de canaux avec le trafic QA interne.
Users are offline during the check
Symptôme : l'application affiche un état de mise à jour corrompu lorsque l'utilisateur l'ouvre sans connectivité.
Cause probable : le chemin de vérification suppose un succès réseau et mappe l'échec vers une interface d'erreur au lieu d'un état neutre.
Solution : dégrader proprement. Maintenez la version actuelle en cours d'exécution, enregistrez l'échec de la vérification et réessayez plus tard lorsque l'application redeviendra active.
Le mode hors ligne est une condition d'exécution normale, pas une exception.
Pour les mises à jour forcées, le mode hors ligne nécessite une attention particulière. Si la version minimale supportée est déjà invalide, l'application peut devoir rester bloquée. Dans ce cas, expliquez clairement la raison et proposez une action de réessai dès que la connectivité revient. Si la mise à jour est optionnelle, ne pénalisez jamais l'utilisateur pour une perte de réseau temporaire.
Le principe récurrent dans tous ces cas est simple : séparer détection, politique, UI, et activation. Lorsque ces enjeux se résument à un seul hook ou composant d'écran, le débogage devient une devinette.
Si votre équipe déploie des apps Capacitor ou Electron et que vous avez besoin d'un système de mise à jour contrôlé avec des canaux, la livraison de bundles signés, la protection contre le rollback et l'observabilité au niveau de l'appareil, Capgo vaut la peine d'être évalué. Il convient aux équipes qui veulent que les live updates se comportent comme une infrastructure de release plutôt que comme un projet secondaire bricolé.
Continuer depuis Stratégies efficaces de notification de mise à jour d'app
Si vous utilisez Stratégies efficaces de notification de mise à jour d'app pour planifier l'automatisation CI/CD, le relier à CI/CD Capgo pour le workflow produit dans le CI/CD de Capgo, Capgo Native Build pour le workflow produit dans les Capgo Native Builds, Intégrations Capgo pour le workflow produit dans les intégrations Capgo, Intégration CI/CD pour les détails d'implémentation de l'intégration CI/CD, et Intégration GitHub Actions pour les détails d'implémentation de l'intégration GitHub Actions.