<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robin</title>
    <description>The latest articles on DEV Community by Robin (@robingenz).</description>
    <link>https://dev.to/robingenz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F366375%2F4d5117ce-2de2-42d6-9fc7-af64a6c61139.jpg</url>
      <title>DEV Community: Robin</title>
      <link>https://dev.to/robingenz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robingenz"/>
    <language>en</language>
    <item>
      <title>Run an On-Device LLM in Your Capacitor App Without an API Key</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Sat, 12 Sep 2026 17:00:53 +0000</pubDate>
      <link>https://dev.to/capawesome/run-an-on-device-llm-in-your-capacitor-app-without-an-api-key-1gg4</link>
      <guid>https://dev.to/capawesome/run-an-on-device-llm-in-your-capacitor-app-without-an-api-key-1gg4</guid>
      <description>&lt;p&gt;Adding an AI feature to a mobile app usually means sending the user's text to a server. You pick a provider, find somewhere for the API key that isn't the app bundle (in practice, a proxy you now run), and accept a bill that grows with every token.&lt;/p&gt;

&lt;p&gt;Modern phones offer another route. iOS 26 ships Apple Intelligence behind the Foundation Models framework, and recent Android devices run Gemini Nano through ML Kit and AICore. Both models belong to the operating system, so the prompt, the conversation history and the answer stay on the device. There is no key to protect and no per-request cost.&lt;/p&gt;

&lt;p&gt;We built the Capacitor LLM plugin to put one TypeScript API over both. To be upfront, it is our plugin and it ships as part of Capawesome Insiders, a paid subscription. This post condenses the &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-llm-plugin/" rel="noopener noreferrer"&gt;full announcement on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What On-Device Inference Buys You
&lt;/h2&gt;

&lt;p&gt;Privacy is the headline, but three other things follow from keeping the model on the phone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No per-token cost.&lt;/strong&gt; Inference runs on hardware the user already paid for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline generation.&lt;/strong&gt; Responses come back in airplane mode or with no reception at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No app size increase.&lt;/strong&gt; The model is part of the OS, so the feature adds nothing to your download.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A system model is small next to a frontier model behind an HTTP endpoint, the usable context is roughly 4,000 tokens on both platforms, and the hardware requirements exclude most devices in use today. That fits summarizing a note or rewriting a paragraph, not reasoning across a long document.&lt;/p&gt;

&lt;h2&gt;
  
  
  One API Over Two System Models
&lt;/h2&gt;

&lt;p&gt;The plugin ships neither a model nor a runtime. It calls each vendor's on-device API and normalizes the result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Underlying API&lt;/th&gt;
&lt;th&gt;Requirements&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Gemini Nano&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://developers.google.com/ml-kit/genai/prompt/android" rel="noopener noreferrer"&gt;ML Kit GenAI Prompt&lt;/a&gt; via AICore&lt;/td&gt;
&lt;td&gt;API level 26+, Gemini Nano-capable device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Apple Intelligence&lt;/td&gt;
&lt;td&gt;&lt;a href="https://developer.apple.com/documentation/foundationmodels" rel="noopener noreferrer"&gt;Foundation Models&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;iOS 26+, iPhone 15 Pro or later, Xcode 26 to build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getAvailability()&lt;/code&gt; resolves &lt;code&gt;unavailable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details shape a release plan. Google's ML Kit GenAI Prompt SDK is still in beta, so the plugin pins &lt;code&gt;com.google.mlkit:genai-prompt&lt;/code&gt; at &lt;code&gt;1.0.0-beta2&lt;/code&gt; and lets you override it with the &lt;code&gt;$mlkitGenaiPromptVersion&lt;/code&gt; Gradle variable. And Gemini Nano runs on few devices today, roughly the Pixel 9 and Galaxy S25 series, which makes the availability check mandatory.&lt;/p&gt;

&lt;p&gt;If you only ship Android, the free &lt;a href="https://capawesome.io/docs/sdks/capacitor/mlkit/genai-prompt/" rel="noopener noreferrer"&gt;Capacitor ML Kit GenAI Prompt plugin&lt;/a&gt; wraps the same Google API directly. For the iOS side, our guide on &lt;a href="https://capawesome.io/blog/how-to-use-apple-intelligence-in-a-capacitor-app/" rel="noopener noreferrer"&gt;using Apple Intelligence in a Capacitor app&lt;/a&gt; walks through the Foundation Models path on a real device, so I won't repeat it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability Is the First Call You Make
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;getAvailability()&lt;/code&gt; never rejects. On a platform or OS version with no system model it resolves with &lt;code&gt;unavailable&lt;/code&gt;, so you can call it at startup and branch on it. Each of the seven statuses maps to a different UI action:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Reported on&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android, iOS&lt;/td&gt;
&lt;td&gt;Start generating.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device-not-eligible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Fall back to a cloud model.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;downloadable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Call &lt;code&gt;downloadModel()&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;downloading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Show progress and wait.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Point the user to Settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;Try again later.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android, iOS, Web&lt;/td&gt;
&lt;td&gt;Hide the feature.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The status changes while your app runs. A user can switch on Apple Intelligence in Settings, or an Android download can finish, and the &lt;code&gt;availabilityChange&lt;/code&gt; event covers both cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchAvailability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAvailability&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;updateUi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;availabilityChange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateUi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch over all seven statuses, not the four Android reports or the five iOS reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downloading Gemini Nano on Android
&lt;/h2&gt;

&lt;p&gt;When the status is &lt;code&gt;downloadable&lt;/code&gt;, the model has to reach the device first, and Android is the only platform where your app starts it. &lt;code&gt;downloadModel()&lt;/code&gt; resolves on completion, and &lt;code&gt;downloadProgress&lt;/code&gt; reports a value between &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt;. Show that progress, because the download runs long enough that a silent button reads as broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;downloadModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downloadProgress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Chats Hold the Conversation Context
&lt;/h2&gt;

&lt;p&gt;A chat is the unit of context. &lt;code&gt;createChat(...)&lt;/code&gt; returns an identifier you pass to every generation, and optionally takes &lt;code&gt;instructions&lt;/code&gt;, the system prompt that shapes the model's role. Pass your own &lt;code&gt;id&lt;/code&gt; to map chats onto your data model, or leave it out and get a UUID.&lt;/p&gt;

&lt;p&gt;The implementation differs underneath. On iOS each chat is backed by a native language model session. On Android the system API has no multi-turn concept, so the plugin keeps the history in memory and includes it in each prompt. Either way, history does not survive an app restart, and every chat holds native resources until you release it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startChat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful assistant that answers briefly.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endChat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deleteChat(...)&lt;/code&gt; also cancels an in-flight generation for that chat, so tearing down a chat screen mid-response is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating and Streaming Text
&lt;/h2&gt;

&lt;p&gt;There are two ways to get text out. &lt;code&gt;generateText(...)&lt;/code&gt; resolves with the complete response and suits short outputs where a spinner is fine. &lt;code&gt;streamText(...)&lt;/code&gt; emits chunks through the &lt;code&gt;textChunk&lt;/code&gt; event and resolves with the full text at the end. Every event carries the &lt;code&gt;chatId&lt;/code&gt; it belongs to, so one listener can serve several chats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;streamAnswer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textChunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;appendToUi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize this note in three bullet points.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one generation runs per chat at a time. A second prompt sent into a busy chat rejects with &lt;code&gt;GENERATION_IN_PROGRESS&lt;/code&gt; instead of queueing, so disable the send button while a response is in flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canceling a Generation
&lt;/h2&gt;

&lt;p&gt;A small model can spend several seconds on an answer nobody wants any more. &lt;code&gt;cancelGeneration(...)&lt;/code&gt; stops the current generation, and the pending promise rejects with &lt;code&gt;GENERATION_CANCELED&lt;/code&gt;, a normal outcome rather than an error to report. On iOS it stops immediately. On Android cancellation is best-effort, so a few more &lt;code&gt;textChunk&lt;/code&gt; events may arrive afterwards. Track a canceled flag per chat and drop those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canceledChatIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stopGeneration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;canceledChatIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelGeneration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where the Platform Limits Bite
&lt;/h2&gt;

&lt;p&gt;Two generation parameters are exposed. &lt;code&gt;maxOutputTokens&lt;/code&gt; caps the length of a response and &lt;code&gt;temperature&lt;/code&gt; controls how deterministic it is. Both work as chat defaults in &lt;code&gt;createChat(...)&lt;/code&gt; and as per-request overrides. The ranges come from the platforms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Android (Gemini Nano)&lt;/th&gt;
&lt;th&gt;iOS (Apple Intelligence)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxOutputTokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum of &lt;code&gt;4096&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;No documented limit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Between &lt;code&gt;0.0&lt;/code&gt; and &lt;code&gt;1.0&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Values above &lt;code&gt;1.0&lt;/code&gt; allowed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context size&lt;/td&gt;
&lt;td&gt;Input under ~4,000 tokens.&lt;/td&gt;
&lt;td&gt;~4,096 tokens per session.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stay inside the Android ranges if you want one code path across both platforms. The context limit is the one to design around: a long conversation eventually overruns it, the generation rejects with &lt;code&gt;GENERATION_FAILED&lt;/code&gt;, and the fix is a new chat seeded with a summary generated while there was still room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;getAvailability()&lt;/code&gt; and a fallback path, because on most devices today the answer is still &lt;code&gt;unavailable&lt;/code&gt;. Once that gate is in place, wire up &lt;code&gt;generateText(...)&lt;/code&gt; first, then &lt;code&gt;streamText(...)&lt;/code&gt; and &lt;code&gt;cancelGeneration(...)&lt;/code&gt; when the UI calls for them.&lt;/p&gt;

&lt;p&gt;Every method and error code lives in the &lt;a href="https://capawesome.io/docs/sdks/capacitor/llm/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt;, and the longer write-up is in the &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-llm-plugin/" rel="noopener noreferrer"&gt;announcement post&lt;/a&gt;. If you have shipped an on-device feature already, tell me in the comments which platform limit you hit first.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>ai</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>The Capacitor AdMob Plugin Built on Google's Next-Gen Ads SDK</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:05:47 +0000</pubDate>
      <link>https://dev.to/capawesome/the-capacitor-admob-plugin-built-on-googles-next-gen-ads-sdk-5228</link>
      <guid>https://dev.to/capawesome/the-capacitor-admob-plugin-built-on-googles-next-gen-ads-sdk-5228</guid>
      <description>&lt;p&gt;Google runs two Android ad SDKs in parallel right now, and one of them has an expiry date. If you are picking an AdMob library for a Capacitor app this year, that single fact should decide it for you.&lt;/p&gt;

&lt;p&gt;We've released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/admob/" rel="noopener noreferrer"&gt;Capacitor AdMob plugin&lt;/a&gt;, the first Capacitor AdMob plugin built on Google's Next-Gen Mobile Ads SDK. This is our plugin, and it ships as part of Capawesome Insiders, a paid subscription. Everything below is the short version of &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-admob-plugin/" rel="noopener noreferrer"&gt;the full announcement on our blog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Next-Gen SDK Matters Now
&lt;/h2&gt;

&lt;p&gt;The legacy Google Mobile Ads SDK on Android sits at major versions 24 and 25 today, and Google has published a &lt;a href="https://developers.google.com/admob/android/deprecation" rel="noopener noreferrer"&gt;deprecation schedule&lt;/a&gt; for both. They reach their deprecation date on June 30, 2027 and their sunset date on June 30, 2028. Google's own wording for a sunset version is that ads are "at risk of not serving", with sunset ad requests returning a no fill.&lt;/p&gt;

&lt;p&gt;The successor is the &lt;a href="https://developers.google.com/admob/android/next-gen" rel="noopener noreferrer"&gt;GMA Next-Gen SDK&lt;/a&gt;. It ships as a new Kotlin-first artifact, &lt;code&gt;com.google.android.libraries.ads.mobile.sdk&lt;/code&gt;, with a different API surface instead of another major version of the old library, which is why plugin authors have to port rather than bump a version number.&lt;/p&gt;

&lt;p&gt;Our plugin runs on the Next-Gen SDK on Android from its first release, defaulting to &lt;code&gt;ads-mobile-sdk&lt;/code&gt; 1.2.1, and on the latest Google Mobile Ads SDK on iOS, where Google has not shipped a Next-Gen variant yet. Both dependency versions are Gradle project variables, so you can pin &lt;code&gt;$adsMobileSdkVersion&lt;/code&gt; and &lt;code&gt;$userMessagingPlatformVersion&lt;/code&gt; in &lt;code&gt;variables.gradle&lt;/code&gt; when another plugin drags in a different version.&lt;/p&gt;

&lt;p&gt;Two years sounds like plenty of runway. It is not, if the port lands in the same quarter as a store deadline. Starting a new monetization layer on a library with a published sunset date means paying for it twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Ad Formats, One Load and Show API
&lt;/h2&gt;

&lt;p&gt;The plugin covers the five AdMob formats Google offers for mobile apps: banner, interstitial, rewarded, rewarded interstitial, and app open. Every full-screen format follows the same two steps, a load method that returns an identifier and a show method that takes it, so learning one teaches you the other three.&lt;/p&gt;

&lt;p&gt;Here is a rewarded ad, with the reward listener registered before the ad goes on screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-admob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showRewardedAd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rewardEarned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;grantReward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadRewardedAd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;adUnitId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ca-app-pub-3940256099942544/5224354917&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showRewardedAd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;loadRewardedAd&lt;/code&gt; for &lt;code&gt;loadInterstitialAd&lt;/code&gt;, &lt;code&gt;loadRewardedInterstitialAd&lt;/code&gt;, or &lt;code&gt;loadAppOpenAd&lt;/code&gt; and the surrounding code stays the same. Because each load call hands back its own identifier, you can keep several ads in flight at once, say one interstitial preloaded for the end of a level and one rewarded ad sitting behind a "watch for coins" button. Pass your own &lt;code&gt;id&lt;/code&gt; if you prefer names you control over generated ones.&lt;/p&gt;

&lt;p&gt;Both rewarded formats accept a &lt;code&gt;serverSideVerification&lt;/code&gt; option with &lt;code&gt;userId&lt;/code&gt; and &lt;code&gt;customData&lt;/code&gt;, forwarded to your verification callback so the reward is granted on what Google reports rather than what the client claims. App open ads get a shortcut of their own: &lt;code&gt;enableAppOpenAutoShow(...)&lt;/code&gt; loads and shows one whenever the app returns to the foreground, with a &lt;code&gt;minInterval&lt;/code&gt; frequency cap that defaults to 14400 seconds, and it stays quiet while a consent form or another full-screen ad is visible.&lt;/p&gt;

&lt;p&gt;AdMob is a mobile product, so every method is Android and iOS only and rejects with an unimplemented error on the web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Banner Ads in Three Modes
&lt;/h2&gt;

&lt;p&gt;Banners are where most Capacitor ad integrations get ugly, because the banner is a native view sitting on top of a web view that knows nothing about it. The plugin gives you three placements, and the right one depends on how much your CSS already knows about the ad.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mode: 'overlay'&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Anchored to the top or bottom edge on top of the web view, aware of safe area insets&lt;/td&gt;
&lt;td&gt;Your layout already reserves the space, for example a fixed footer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode: 'resize'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The web view shrinks so the banner never covers web content&lt;/td&gt;
&lt;td&gt;You want the banner clear of your content without touching your CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;frame: { x, y, width, height }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The banner lands at a rectangle you measure in CSS pixels&lt;/td&gt;
&lt;td&gt;Inline placement inside content, for example between list items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Resize mode is the shortest path to a banner that never hides a button, and it needs the ad unit, a size, and a position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BannerSize&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-admob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;showBanner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showBanner&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;adUnitId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ca-app-pub-3940256099942544/6300978111&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BannerSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AdaptiveBanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For inline placement you measure an anchor element with &lt;code&gt;getBoundingClientRect()&lt;/code&gt;, pass the rectangle as &lt;code&gt;frame&lt;/code&gt;, and call &lt;code&gt;setBannerFrame(...)&lt;/code&gt; when the layout shifts. Hold on to the returned identifier, because &lt;code&gt;hideBanner(...)&lt;/code&gt;, &lt;code&gt;resumeBanner(...)&lt;/code&gt;, and &lt;code&gt;removeBanner(...)&lt;/code&gt; all operate on it, and it is what lets you run more than one banner at a time.&lt;/p&gt;

&lt;p&gt;Seven sizes are available. &lt;code&gt;AdaptiveBanner&lt;/code&gt; matches the screen or frame width with a height chosen by the SDK, &lt;code&gt;InlineAdaptiveBanner&lt;/code&gt; is capped by the frame height you pass, and the fixed sizes cover 320x50, 320x100, 300x250, plus 468x60 and 728x90 for tablets. Set &lt;code&gt;collapsible: true&lt;/code&gt; for a banner that expands into a larger ad and collapses back, then listen for &lt;code&gt;bannerSizeChanged&lt;/code&gt; to keep your layout honest when it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent in a Single Call
&lt;/h2&gt;

&lt;p&gt;Google requires User Messaging Platform consent before you request a single ad from users in the European Economic Area, the UK, or a regulated US state. The canonical flow is a request for consent information followed by a conditional form presentation, and &lt;code&gt;requestConsent(...)&lt;/code&gt; does both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-admob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setupAds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;canRequestAds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;privacyOptionsRequired&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestConsent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canRequestAds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;privacyOptionsRequired&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it on every app launch, before &lt;code&gt;initialize(...)&lt;/code&gt;, because the answer changes over time. Until the consent requirements are met, every load method rejects with &lt;code&gt;CONSENT_NOT_GATHERED&lt;/code&gt;, so a non-compliant ad request never leaves the device. When &lt;code&gt;privacyOptionsRequired&lt;/code&gt; comes back &lt;code&gt;true&lt;/code&gt;, give the user a settings entry that calls &lt;code&gt;showPrivacyOptionsForm()&lt;/code&gt;. For the AdMob console setup, the App Tracking Transparency ordering on iOS, and the error cases, we wrote a separate walkthrough on &lt;a href="https://capawesome.io/blog/how-to-handle-admob-gdpr-consent-in-a-capacitor-app/" rel="noopener noreferrer"&gt;handling AdMob GDPR consent in a Capacitor app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impression-Level Revenue for Every Format
&lt;/h2&gt;

&lt;p&gt;Ten events cover the ad lifecycle, most of them carrying the ad's &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;format&lt;/code&gt; so one listener can serve all five formats. The one to wire up first is &lt;code&gt;adRevenuePaid&lt;/code&gt;, because LTV models, ROAS dashboards, and cohort analyses all run on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-admob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trackAdRevenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Admob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adRevenuePaid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ad_revenue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;precision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;value&lt;/code&gt; is the amount in the currency's standard unit, &lt;code&gt;currencyCode&lt;/code&gt; is an ISO 4217 code, and &lt;code&gt;precision&lt;/code&gt; tells you how much to trust the number: &lt;code&gt;PRECISE&lt;/code&gt;, &lt;code&gt;ESTIMATED&lt;/code&gt;, &lt;code&gt;PUBLISHER_PROVIDED&lt;/code&gt;, or &lt;code&gt;UNKNOWN&lt;/code&gt;. Mixing estimated and precise values into one total produces a figure you cannot reconcile against your AdMob reports later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Switch?
&lt;/h2&gt;

&lt;p&gt;If you are starting an AdMob integration today, start on the Next-Gen SDK. If you already ship ads with &lt;code&gt;@capacitor-community/admob&lt;/code&gt; and your setup is one anchored banner plus the occasional interstitial, there is no urgency. Revisit when you need resize or inline banners, several concurrent ad instances, or revenue events across every format. The API mapping is mechanical when you do: &lt;code&gt;prepareInterstitial&lt;/code&gt; becomes &lt;code&gt;loadInterstitialAd&lt;/code&gt;, &lt;code&gt;prepareRewardVideoAd&lt;/code&gt; becomes &lt;code&gt;loadRewardedAd&lt;/code&gt;, and &lt;code&gt;adId&lt;/code&gt; becomes &lt;code&gt;adUnitId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The plugin is still marked experimental, so the API surface is complete and typed but has not been through heavy production testing, which makes bug reports genuinely valuable right now.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://capawesome.io/docs/sdks/capacitor/admob/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; has the full API reference, and the &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-admob-plugin/" rel="noopener noreferrer"&gt;announcement post&lt;/a&gt; covers the parts I skipped here, including test ad units, typed error codes, and the native setup on both platforms. If you try it, tell me what breaks.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Android Scoped Storage in Capacitor Apps: What Still Works</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:00:33 +0000</pubDate>
      <link>https://dev.to/capawesome/android-scoped-storage-in-capacitor-apps-what-still-works-13i4</link>
      <guid>https://dev.to/capawesome/android-scoped-storage-in-capacitor-apps-what-still-works-13i4</guid>
      <description>&lt;p&gt;&lt;code&gt;Directory.ExternalStorage&lt;/code&gt; in &lt;code&gt;@capacitor/filesystem&lt;/code&gt; is documented as inaccessible on Android 11 and newer, and no permission brings it back. Code that wrote a PDF into &lt;code&gt;Download/&lt;/code&gt; in 2020 still compiles and fails at runtime on every current device. That is not a plugin bug, it is the storage model Android enforces for every app that targets Android 11 (API level 30) or higher.&lt;/p&gt;

&lt;p&gt;This is the condensed version of our longer write-up, &lt;a href="https://capawesome.io/blog/android-scoped-storage-in-capacitor-apps/" rel="noopener noreferrer"&gt;Android Scoped Storage in Capacitor Apps, Explained&lt;/a&gt;. One disclosure first. The &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-manager/" rel="noopener noreferrer"&gt;Capacitor File Manager plugin&lt;/a&gt; in the code samples is ours, and it is part of Capawesome Insiders, a paid subscription. The Android behavior below applies either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Android changed, and when
&lt;/h2&gt;

&lt;p&gt;Scoped storage limits an app to its own directory on external storage plus the media files it created itself. Android made it the default for apps that target &lt;a href="https://developer.android.com/training/data-storage#scoped-storage" rel="noopener noreferrer"&gt;Android 10 (API level 29)&lt;/a&gt; and higher, so an app can no longer walk the shared volume and read whatever it finds there.&lt;/p&gt;

&lt;p&gt;Android 10 still allowed an opt-out. An app could set &lt;code&gt;android:requestLegacyExternalStorage="true"&lt;/code&gt; in its manifest and keep the old behavior. &lt;a href="https://developer.android.com/about/versions/11/privacy/storage" rel="noopener noreferrer"&gt;Android 11 (API level 30)&lt;/a&gt; ignores that flag as soon as your app targets Android 11, and the same page states that both &lt;code&gt;WRITE_EXTERNAL_STORAGE&lt;/code&gt; and the privileged &lt;code&gt;WRITE_MEDIA_STORAGE&lt;/code&gt; permission stop providing additional access at that target level.&lt;/p&gt;

&lt;p&gt;You cannot sit this out. Since 31 August 2026, &lt;a href="https://developer.android.com/google/play/requirements/target-sdk" rel="noopener noreferrer"&gt;Google Play requires&lt;/a&gt; new apps and app updates to target Android 16 (API level 36) or higher, six releases past the point where the legacy flag became dead configuration.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Download&lt;/code&gt; directory is the one everybody asks about. Android 11 removed it from what the Storage Access Framework hands out. The system picker still lists it, but Google's own test instructions describe the expected result as the directory showing up with "the action button associated with the directory grayed out". The internal storage root and the roots of reliable SD card volumes are blocked the same way. For a file that has to land there, use the &lt;code&gt;MediaStore.Downloads&lt;/code&gt; collection instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still works in @capacitor/filesystem
&lt;/h2&gt;

&lt;p&gt;Most of the plugin is unaffected. Scoped storage restricts shared storage, not the app sandbox, so app-specific directories behave as before Android 10 and need no permission.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Directory&lt;/th&gt;
&lt;th&gt;Status on Android 11 and newer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.Data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works. Maps to &lt;code&gt;getFilesDir()&lt;/code&gt;, private to the app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.Cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works. Maps to &lt;code&gt;getCacheDir()&lt;/code&gt;, may be reclaimed by the system.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.Library&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works. Maps to &lt;code&gt;getFilesDir()&lt;/code&gt; on Android.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.External&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works. App-specific directory on the shared volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.ExternalCache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Works. App-specific cache on the shared volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.Documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partly. Your app only sees files and folders it created itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Directory.ExternalStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gone. Documented as not accessible on Android 11 or newer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two issues in &lt;code&gt;ionic-team/capacitor-filesystem&lt;/code&gt; have tracked this gap for years without a resolution. &lt;a href="https://github.com/ionic-team/capacitor-filesystem/issues/28" rel="noopener noreferrer"&gt;#28&lt;/a&gt;, asking the plugin to accommodate scoped storage, was opened on 30 December 2020. &lt;a href="https://github.com/ionic-team/capacitor-filesystem/issues/37" rel="noopener noreferrer"&gt;#37&lt;/a&gt; asks which &lt;code&gt;Directory&lt;/code&gt; member to use for which purpose now that half the enum behaves differently per Android version. Until one of them lands, treat &lt;code&gt;@capacitor/filesystem&lt;/code&gt; as a sandbox API that stops at the boundary of shared storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MANAGE_EXTERNAL_STORAGE is the wrong fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MANAGE_EXTERNAL_STORAGE&lt;/code&gt; is the "all files access" permission Android 11 introduced, and it does restore broad access to shared storage. It is also the declaration most likely to stall a Play Store review. Google Play has &lt;a href="https://developer.android.com/training/data-storage/manage-all-files" rel="noopener noreferrer"&gt;evaluated apps that declare it&lt;/a&gt; under a dedicated policy since May 2021, and Google's guidance is to request it only when the app cannot do its job through the Storage Access Framework or the MediaStore API.&lt;/p&gt;

&lt;p&gt;The permitted uses are narrow and tied to core functionality. File managers, backup tools, anti-virus apps, and similar categories where browsing arbitrary files is the product. An expense app that drops receipts into a folder is not in that group, and reviewers treat document management as a use case the Storage Access Framework already covers. The permission does not widen the Storage Access Framework either, so it buys you nothing there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Storage Access Framework replaces shared paths
&lt;/h2&gt;

&lt;p&gt;The Storage Access Framework turns "give me a path" into "let the user grant me a folder". Your app fires an &lt;a href="https://developer.android.com/training/data-storage/shared/documents-files" rel="noopener noreferrer"&gt;&lt;code&gt;ACTION_OPEN_DOCUMENT_TREE&lt;/code&gt;&lt;/a&gt; intent, available since Android 5.0 (API level 21), and the user picks a directory in the system picker. What comes back is a tree URI covering that directory and everything beneath it, and nothing else.&lt;/p&gt;

&lt;p&gt;That grant expires when the device restarts. Calling &lt;code&gt;takePersistableUriPermission()&lt;/code&gt; with the read and write flags makes it survive reboots and app restarts, which turns a one-off picker result into a folder your app keeps using. Android drops it again if the document behind it is moved or deleted, and the user then has to pick a folder again.&lt;/p&gt;

&lt;p&gt;Nothing in this flow needs a permission in your manifest. The user's choice in the system picker is the permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisting a folder with the File Manager plugin
&lt;/h2&gt;

&lt;p&gt;The Capacitor File Manager plugin puts that flow behind two methods, so you never touch a tree URI or a permission flag yourself. Let the user pick a directory with &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-picker/#pickdirectory" rel="noopener noreferrer"&gt;&lt;code&gt;pickDirectory()&lt;/code&gt;&lt;/a&gt; from the Capacitor File Picker plugin, then pass the result to &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-manager/#persistdirectoryaccess" rel="noopener noreferrer"&gt;&lt;code&gt;persistDirectoryAccess(...)&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-file-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FilePicker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome/capacitor-file-picker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pickExportFolder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FilePicker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persistDirectoryAccess&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bookmark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bookmark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;bookmark&lt;/code&gt; value is the iOS half of the same idea, a security-scoped bookmark. Android ignores it, because the persisted tree URI already carries the grant.&lt;/p&gt;

&lt;p&gt;Do not store the returned URI yourself, because it can change between app launches. Read the current list on startup with &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-manager/#getpersisteddirectories" rel="noopener noreferrer"&gt;&lt;code&gt;getPersistedDirectories()&lt;/code&gt;&lt;/a&gt; instead, which drops directories whose document no longer exists. Every method of the plugin accepts a persisted URI. Build the URI of a file inside the folder with &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-manager/#geturi" rel="noopener noreferrer"&gt;&lt;code&gt;getUri(...)&lt;/code&gt;&lt;/a&gt; by passing &lt;code&gt;parentUri&lt;/code&gt; instead of &lt;code&gt;directory&lt;/code&gt;, then copy, move or read as usual.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-file-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exportReport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directoryUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUri&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;report.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUri&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exports/report.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parentUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;directoryUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetUri&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to watch. Constructing the URI of an entry that does not exist yet works only for path-structured document providers such as local device storage, so a cloud provider mounted into the picker may reject it. And &lt;code&gt;copyFile(...)&lt;/code&gt; returns the URI of the created file, which differs from the one you requested if the provider renamed the file to avoid a collision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which directory or API should you use?
&lt;/h2&gt;

&lt;p&gt;Pick by who owns the file and who needs to see it, not by which directory name sounds closest to the old one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;th&gt;What to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App data that must survive updates and stay private&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Directory.Data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files you can regenerate at any time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Directory.Cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large app-owned files on the shared volume&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Directory.External&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A user-visible folder your app keeps using across launches&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pickDirectory()&lt;/code&gt; plus &lt;code&gt;persistDirectoryAccess(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A single file the user selects once&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pickFiles(...)&lt;/code&gt; from the File Picker plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Photos, videos or audio in the gallery&lt;/td&gt;
&lt;td&gt;MediaStore collections (platform API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A download the user finds in &lt;code&gt;Download/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;MediaStore.Downloads&lt;/code&gt; collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browsing arbitrary files across the device&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MANAGE_EXTERNAL_STORAGE&lt;/code&gt;, only as core functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If your app writes outside its own sandbox on Android, stop hunting for a directory constant that still works. Grep the project for &lt;code&gt;Directory.ExternalStorage&lt;/code&gt; and &lt;code&gt;Directory.Documents&lt;/code&gt;, move those call sites to a picked and persisted directory, and keep &lt;code&gt;Directory.Data&lt;/code&gt; and &lt;code&gt;Directory.Cache&lt;/code&gt; for everything the user never has to see. That split holds for every Android version you still support, so you do the work once.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://capawesome.io/blog/android-scoped-storage-in-capacitor-apps/" rel="noopener noreferrer"&gt;full guide&lt;/a&gt; goes deeper on the permission history, and our &lt;a href="https://capawesome.io/blog/capacitor-file-handling-guide/" rel="noopener noreferrer"&gt;Capacitor file handling guide&lt;/a&gt; covers reading, writing and sharing files without running out of memory. If your case does not fit a row of the table above, drop it in the comments.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>android</category>
      <category>mobile</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Apple Intelligence in a Capacitor App: On-Device Text Generation</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 09 Sep 2026 17:00:52 +0000</pubDate>
      <link>https://dev.to/capawesome/apple-intelligence-in-a-capacitor-app-on-device-text-generation-45e1</link>
      <guid>https://dev.to/capawesome/apple-intelligence-in-a-capacitor-app-on-device-text-generation-45e1</guid>
      <description>&lt;p&gt;Sending user text to a hosted language model costs money on every request, adds latency, and means notes and messages leave the phone. Since iOS 26, Apple's &lt;a href="https://developer.apple.com/documentation/foundationmodels" rel="noopener noreferrer"&gt;Foundation Models framework&lt;/a&gt; opens the on-device model behind Writing Tools to third-party apps, so a summarize button can run offline, for free, with no API key to rotate. Here is how to reach that model from a Capacitor app without writing Swift.&lt;/p&gt;

&lt;p&gt;Full disclosure: the &lt;a href="https://capawesome.io/docs/sdks/capacitor/llm/" rel="noopener noreferrer"&gt;Capacitor LLM plugin&lt;/a&gt; used throughout this post is ours, and it is part of Capawesome Insiders, a paid subscription. You need a license key to install it from the Capawesome npm registry. Apple's model itself costs nothing per request. This is the condensed version of &lt;a href="https://capawesome.io/blog/how-to-use-apple-intelligence-in-a-capacitor-app/" rel="noopener noreferrer"&gt;How to Use Apple Intelligence in a Capacitor App&lt;/a&gt; from our blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the plugin exposes
&lt;/h2&gt;

&lt;p&gt;The plugin wraps the Foundation Models framework in one TypeScript API. From your web code you get chat sessions that keep conversation context and carry instructions, token streaming through an event, cancellation of a running generation, a typed availability status, and per-chat or per-request &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;maxOutputTokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things Apple's framework offers are not exposed yet: guided generation into typed structures and tool calling. Plan for free-text responses.&lt;/p&gt;

&lt;p&gt;Inference runs on the Neural Engine. The plugin bundles no model files, makes no network calls, and adds no data type to your privacy nutrition label. It calls only public platform APIs, so there is nothing extra to explain to App Review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;Hardware alone does not decide whether the model runs. Four other conditions have to hold as well:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;iPhone 15 Pro or later, iPad mini (A17 Pro) or M1 iPads and later, Apple silicon Macs, Apple Vision Pro. Apple keeps the current list on its &lt;a href="https://support.apple.com/en-us/121115" rel="noopener noreferrer"&gt;Apple Intelligence support page&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS version&lt;/td&gt;
&lt;td&gt;iOS 26, iPadOS 26, macOS 26, or visionOS 26. Apple Intelligence shipped with iOS 18.1, but the developer API did not.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple Intelligence turned on&lt;/td&gt;
&lt;td&gt;The user enables it under Settings &amp;gt; Apple Intelligence &amp;amp; Siri. Until then the plugin reports &lt;code&gt;not-enabled&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;About 7 GB of free space for the model download, which the system manages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build toolchain&lt;/td&gt;
&lt;td&gt;Xcode 26 or later, plus Capacitor 8 for the plugin itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On iOS 18 every method except &lt;code&gt;getAvailability()&lt;/code&gt; rejects as unavailable. On an iPhone 15 or older, the status is &lt;code&gt;device-not-eligible&lt;/code&gt;. Both are normal states your UI has to handle, which is where the first step comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check availability before showing the feature
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;getAvailability()&lt;/code&gt; never rejects. On iOS it resolves with one of five statuses, and each one maps to a different UI decision:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;available&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The model is ready.&lt;/td&gt;
&lt;td&gt;Enable the feature.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device-not-eligible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The hardware cannot run Apple Intelligence.&lt;/td&gt;
&lt;td&gt;Hide the feature or fall back to a server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apple Intelligence is off in Settings.&lt;/td&gt;
&lt;td&gt;Explain how to turn it on, then check again.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-ready&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The system is preparing or downloading the model.&lt;/td&gt;
&lt;td&gt;Show a waiting state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No system model on this OS version.&lt;/td&gt;
&lt;td&gt;Hide the feature or fall back to a server.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small helper turns the status into something a component can render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkAppleIntelligence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAvailability&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;available&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-enabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Turn on Apple Intelligence in Settings to use this feature.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not-ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The on-device model is still being prepared. Try again in a moment.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status changes while your app is open, for example when the user enables Apple Intelligence in Settings and switches back. Listen for the &lt;code&gt;availabilityChange&lt;/code&gt; event and re-run the check when it fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watchAvailability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;availabilityChange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin watches the system status only while a listener is attached, so remove it when the screen goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a chat with instructions
&lt;/h2&gt;

&lt;p&gt;Every generation belongs to a chat. On iOS a chat is backed by a native model session that keeps the conversation context, so a follow-up prompt can refer to the previous answer. Pass instructions to define the role and the output format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createSummaryChat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You summarize notes into at most three short bullet points. Keep the language of the note.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; is optional, and passing your own (one per document, say) makes a second &lt;code&gt;createChat(...)&lt;/code&gt; with the same id reject with &lt;code&gt;CHAT_ALREADY_EXISTS&lt;/code&gt;. Chats live in memory and each one holds a native session, so delete a chat when the user leaves the screen with &lt;code&gt;Llm.deleteChat({ id })&lt;/code&gt;. Deleting also cancels a generation still running in that chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Generate a response
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;generateText(...)&lt;/code&gt; sends a prompt into a chat and resolves with the complete response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Summarize this note:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One generation runs per chat at a time. Starting a second before the first resolves rejects with &lt;code&gt;GENERATION_IN_PROGRESS&lt;/code&gt;, so disable the submit button while a request is pending, or give each task its own chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Stream tokens into the UI
&lt;/h2&gt;

&lt;p&gt;For anything longer than a sentence, waiting for the full response feels broken. &lt;code&gt;streamText(...)&lt;/code&gt; emits a &lt;code&gt;textChunk&lt;/code&gt; event for every piece and still resolves with the complete text at the end. Filter by &lt;code&gt;chatId&lt;/code&gt;, because chunks from all chats arrive through the same listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;streamSummary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textChunk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Summarize this note:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append the chunks in arrival order. The resolved text equals the concatenated chunks, so use whichever fits your rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Cancel a generation
&lt;/h2&gt;

&lt;p&gt;A user who can watch a response arrive will want to stop it. &lt;code&gt;cancelGeneration(...)&lt;/code&gt; stops the running generation of a chat, and the pending promise rejects with &lt;code&gt;GENERATION_CANCELED&lt;/code&gt;, which you should treat as a normal outcome rather than an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-llm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelGeneration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;summarizeWithCancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateText&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;note&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GENERATION_CANCELED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS the cancellation takes effect immediately, and the chat stays usable afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits to plan for
&lt;/h2&gt;

&lt;p&gt;Two parameters shape the output. &lt;code&gt;temperature&lt;/code&gt; controls how deterministic it is, &lt;code&gt;maxOutputTokens&lt;/code&gt; caps its length, and both can be set per chat and overridden per request. Their ranges differ by platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;iOS (Apple Intelligence)&lt;/th&gt;
&lt;th&gt;Android (Gemini Nano)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values above &lt;code&gt;1.0&lt;/code&gt; are allowed.&lt;/td&gt;
&lt;td&gt;Between &lt;code&gt;0.0&lt;/code&gt; and &lt;code&gt;1.0&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxOutputTokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No documented hard limit.&lt;/td&gt;
&lt;td&gt;At most &lt;code&gt;4096&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context size&lt;/td&gt;
&lt;td&gt;About 4,096 tokens per chat session.&lt;/td&gt;
&lt;td&gt;Input under about 4,000 tokens.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The context window is the limit you hit first. Instructions, every prompt, and every response count against it, and once it overflows the next generation rejects with &lt;code&gt;GENERATION_FAILED&lt;/code&gt;. The same code appears when Apple's guardrails block a prompt or a response, so read the error message for the platform reason and show the user something better than a silent failure. For a summarizer, one chat per document keeps the window from filling up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same code runs on Android
&lt;/h2&gt;

&lt;p&gt;The plugin uses Gemini Nano through the ML Kit GenAI Prompt API on Android, so the create, generate, stream, and cancel calls above need no changes. Availability differs: Android reports &lt;code&gt;available&lt;/code&gt;, &lt;code&gt;downloadable&lt;/code&gt;, &lt;code&gt;downloading&lt;/code&gt;, and &lt;code&gt;unavailable&lt;/code&gt;, and on &lt;code&gt;downloadable&lt;/code&gt; your app triggers the download itself. Chat history is kept in memory by the plugin, because the Android API has no native multi-turn sessions, and cancellation is best-effort, so a few extra chunks can still arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Gate the feature on &lt;code&gt;getAvailability()&lt;/code&gt;, ship &lt;code&gt;generateText(...)&lt;/code&gt; first, then add streaming and a cancel button together once responses grow past a sentence. Keep one chat per task and the 4,096-token window stays out of your way.&lt;/p&gt;

&lt;p&gt;The API reference for every method and event is in the &lt;a href="https://capawesome.io/docs/sdks/capacitor/llm/" rel="noopener noreferrer"&gt;Capacitor LLM plugin&lt;/a&gt; documentation, and the long-form walkthrough with the full note summarizer class, an availability troubleshooting table, and Simulator notes is in &lt;a href="https://capawesome.io/blog/how-to-use-apple-intelligence-in-a-capacitor-app/" rel="noopener noreferrer"&gt;How to Use Apple Intelligence in a Capacitor App&lt;/a&gt;. If you build something with the on-device model, I would like to read about it in the comments.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>ios</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Ionic Framework MCP Server for Your AI Assistant</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Tue, 08 Sep 2026 15:04:50 +0000</pubDate>
      <link>https://dev.to/capawesome/ionic-framework-mcp-server-for-your-ai-assistant-39kb</link>
      <guid>https://dev.to/capawesome/ionic-framework-mcp-server-for-your-ai-assistant-39kb</guid>
      <description>&lt;p&gt;Ask your AI assistant which events &lt;code&gt;ion-modal&lt;/code&gt; emits and get the v9 answer, with a usage example for your framework. The &lt;a href="https://capawesome.io/docs/ai/mcp/ionic-framework/" rel="noopener noreferrer"&gt;Ionic Framework MCP server&lt;/a&gt; gives the assistant the current documentation for v9 and v8, including the component API reference and the official usage examples for Angular, React, Vue and vanilla JavaScript, instead of whatever was in its training data a release or two ago. It is a community project built and hosted by Capawesome, not an official Ionic product, and it is free to use with no account and no token.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A short screen recording on the &lt;a href="https://capawesome.io/blog/ionic-framework-mcp-server/" rel="noopener noreferrer"&gt;original post&lt;/a&gt; shows Claude Code listing Ionic Framework components through the server.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Ionic Framework MCP server runs at &lt;code&gt;https://ionic-framework-mcp.capawesome.io/mcp&lt;/code&gt;. No account, no token, no installation.&lt;/li&gt;
&lt;li&gt;It serves the Ionic Framework documentation for v9 and v8, the component API reference, the official usage examples for Angular, React, Vue and vanilla JavaScript, and the Ionic blog.&lt;/li&gt;
&lt;li&gt;Five tools: &lt;code&gt;search_docs&lt;/code&gt;, &lt;code&gt;get_doc_page&lt;/code&gt;, &lt;code&gt;list_components&lt;/code&gt;, &lt;code&gt;get_component_usage&lt;/code&gt; and &lt;code&gt;list_blog_posts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;version&lt;/code&gt; parameter pins every answer to the major version your project installs. It defaults to &lt;code&gt;v9&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why assistants get it wrong
&lt;/h2&gt;

&lt;p&gt;Because a model answers from a snapshot of the web, and your project is not on that snapshot. Ionic Framework v9 landed after most training sets were assembled, so an assistant that learned v8 writes v8 property names, v8 event names, and v8 CSS custom properties, and nothing fails loudly. A &lt;code&gt;::part()&lt;/code&gt; selector aimed at a shadow part that the release does not have still compiles and renders, and changes nothing.&lt;/p&gt;

&lt;p&gt;Component APIs are where this hurts most. &lt;code&gt;ion-datetime&lt;/code&gt; alone carries dozens of properties, several events, and a list of CSS shadow parts that moves between major versions. No model holds those tables accurately for two releases at once, and it has no way to know which release you installed.&lt;/p&gt;

&lt;p&gt;With the MCP server connected, the assistant fetches the page it needs on its own and answers from it, without you pasting documentation into the chat or your agent parsing HTML from a web fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the server provides
&lt;/h2&gt;

&lt;p&gt;The server exposes five tools, and the assistant decides on its own which one a question needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search the documentation by keyword.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_doc_page&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a full documentation page as Markdown.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_components&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List Ionic Framework components with their API reference.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_component_usage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read the official usage examples for a component, per framework.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_blog_posts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List recent posts from the Ionic blog.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ask &lt;em&gt;"Which events does &lt;code&gt;ion-modal&lt;/code&gt; emit?"&lt;/em&gt; and the assistant calls &lt;code&gt;search_docs&lt;/code&gt;, then &lt;code&gt;get_doc_page&lt;/code&gt; on the modal page. Back comes the v9 event table, &lt;code&gt;ionModalDidPresent&lt;/code&gt; and &lt;code&gt;ionModalDidDismiss&lt;/code&gt; included, with the exact names instead of plausible ones.&lt;/p&gt;

&lt;p&gt;A component page returned by &lt;code&gt;get_doc_page&lt;/code&gt; carries the full API reference: properties, events, methods, CSS shadow parts, CSS custom properties, and slots. That is the table an assistant needs to style a component correctly instead of inventing a selector that looks plausible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;get_component_usage&lt;/code&gt; returns the official usage examples from the documentation and takes a &lt;code&gt;framework&lt;/code&gt; of &lt;code&gt;angular&lt;/code&gt;, &lt;code&gt;react&lt;/code&gt;, &lt;code&gt;vue&lt;/code&gt; or &lt;code&gt;javascript&lt;/code&gt;. An Angular project gets the Angular template, not a React snippet the model translated on the fly. &lt;code&gt;list_blog_posts&lt;/code&gt; covers the Ionic blog, so release notes and announcements are in reach as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version-aware answers
&lt;/h2&gt;

&lt;p&gt;Every documentation tool accepts a &lt;code&gt;version&lt;/code&gt; parameter, with &lt;code&gt;v9&lt;/code&gt; as the default and &lt;code&gt;v8&lt;/code&gt; as the alternative. Set it to the major version of &lt;code&gt;@ionic/core&lt;/code&gt;, &lt;code&gt;@ionic/angular&lt;/code&gt;, &lt;code&gt;@ionic/react&lt;/code&gt; or &lt;code&gt;@ionic/vue&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;, so the properties and events your assistant suggests exist in the release you ship.&lt;/p&gt;

&lt;p&gt;Put a line in your &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;CLAUDE.md&lt;/code&gt; so the assistant sets it without being asked every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;When using the Ionic Framework MCP server, always pass the &lt;span class="sb"&gt;`version`&lt;/span&gt;
parameter that matches the major version installed in package.json.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;It is a remote HTTP server, so most clients need one line. In &lt;a href="https://www.anthropic.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http ionic-framework https://ionic-framework-mcp.capawesome.io/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor, VS Code, Claude Desktop, Windsurf and Zed each want their own config file, and clients that cannot speak HTTP can run &lt;code&gt;npx -y @capawesome/ionic-framework-mcp&lt;/code&gt; as a stdio proxy to the hosted server. The &lt;a href="https://capawesome.io/docs/ai/mcp/ionic-framework/" rel="noopener noreferrer"&gt;Ionic Framework MCP server&lt;/a&gt; documentation has the exact snippet for each. Once connected, ask your assistant something like &lt;em&gt;"Show me an inline &lt;code&gt;ion-datetime&lt;/code&gt; in React"&lt;/em&gt; to check that the tools are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The server runs as a Cloudflare Worker in front of a search index that is rebuilt daily from &lt;a href="https://github.com/ionic-team/ionic-docs" rel="noopener noreferrer"&gt;&lt;code&gt;ionic-team/ionic-docs&lt;/code&gt;&lt;/a&gt; at a pinned commit, with the source MDX converted to Markdown so pages arrive as text a model can read instead of a mix of JSX components. A daily rebuild means a documentation change reaches your assistant the next day without you touching anything. The client package is open source at &lt;a href="https://github.com/capawesome-team/ionic-framework-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;capawesome-team/ionic-framework-mcp&lt;/code&gt;&lt;/a&gt;, and its issue tracker is the place to report a wrong search result or a missing parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unofficial, with attribution
&lt;/h2&gt;

&lt;p&gt;The server is maintained by Capawesome. It is not affiliated with or endorsed by Ionic or OutSystems, and its documentation page says so at the top.&lt;/p&gt;

&lt;p&gt;The documentation content is © the Ionic team and licensed under &lt;a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noopener noreferrer"&gt;Apache-2.0&lt;/a&gt;. It comes from &lt;a href="https://github.com/ionic-team/ionic-docs" rel="noopener noreferrer"&gt;&lt;code&gt;ionic-team/ionic-docs&lt;/code&gt;&lt;/a&gt;, converted from MDX to Markdown for delivery over MCP and otherwise served unmodified in substance. Blog excerpts come from the &lt;a href="https://ionic.io/blog" rel="noopener noreferrer"&gt;Ionic blog&lt;/a&gt; and link to the original post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capacitor
&lt;/h2&gt;

&lt;p&gt;Most Ionic Framework apps ship on Capacitor, and the same version problem applies there. The Ionic team is working on an official Capacitor MCP server, discussed in &lt;a href="https://github.com/ionic-team/capacitor/discussions/8555" rel="noopener noreferrer"&gt;ionic-team/capacitor#8555&lt;/a&gt;. That discussion is the place to say what it should cover.&lt;/p&gt;

&lt;p&gt;Until it arrives, our &lt;a href="https://capawesome.io/docs/ai/mcp/capacitor/" rel="noopener noreferrer"&gt;Capacitor MCP server&lt;/a&gt; serves the Capacitor documentation on the same terms: free, no token, a &lt;code&gt;version&lt;/code&gt; parameter, a daily rebuild. It is a stopgap so the community has something to point an assistant at today, and its documentation page will link the official server once that ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Add the server to the assistant you already use, then pin the &lt;code&gt;version&lt;/code&gt; parameter in your project rules and leave it there. One command removes a class of wrong answers, the ones that look right because they were right in the previous major.&lt;/p&gt;

&lt;p&gt;If you also ship with Capawesome, add the &lt;a href="https://capawesome.io/docs/ai/mcp/capawesome/" rel="noopener noreferrer"&gt;Capawesome MCP server&lt;/a&gt; next; it covers the Capawesome plugins and Capawesome Cloud, and with an API token it can act on your apps rather than only read documentation. For the wider picture of agent-driven development, read &lt;a href="https://capawesome.io/blog/how-to-use-ai-agents-in-capacitor-app-development/" rel="noopener noreferrer"&gt;How to Use AI Agents in Capacitor App Development&lt;/a&gt;. Questions and bug reports are welcome on the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt;, and the &lt;a href="https://capawesome.io/newsletter/" rel="noopener noreferrer"&gt;Capawesome newsletter&lt;/a&gt; is where the next server gets announced.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://capawesome.io/blog/ionic-framework-mcp-server/" rel="noopener noreferrer"&gt;capawesome.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Scan Documents in Capacitor with ML Kit and VisionKit</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:04:18 +0000</pubDate>
      <link>https://dev.to/capawesome/scan-documents-in-capacitor-with-ml-kit-and-visionkit-39e</link>
      <guid>https://dev.to/capawesome/scan-documents-in-capacitor-with-ml-kit-and-visionkit-39e</guid>
      <description>&lt;p&gt;Document scanning UI is a solved problem: Google and Apple each ship a polished, ML-powered scanner with the operating system — the same flows users know from Google Drive and Apple Notes. What a Capacitor app needed was a bridge, not another camera stack. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/document-scanner/" rel="noopener noreferrer"&gt;Capacitor Document Scanner plugin&lt;/a&gt;, and its surface is deliberately tiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two API calls
&lt;/h2&gt;

&lt;p&gt;Check availability, then scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DocumentScanner&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-document-scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;DocumentScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scannedImages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;DocumentScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scanDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imageQuality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pageLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OS detects the edges, corrects the perspective, and offers a review step before the pages come back as JPEG files in the cache directory. A user backing out is a normal outcome: the promise rejects with &lt;code&gt;SCAN_CANCELED&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a PDF instead of loose images
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;DocumentScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scanDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;generatePdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One combined PDF of all scanned pages — generated by ML Kit on Android, composed from the page images on iOS. Perfect for receipts, signed contracts, and anything a backend expects as a single file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform behavior, documented honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On Android, &lt;code&gt;androidScannerMode&lt;/code&gt; selects the editing capabilities (&lt;code&gt;Base&lt;/code&gt; for crop/rotate, &lt;code&gt;BaseWithFilter&lt;/code&gt; adds filters, &lt;code&gt;Full&lt;/code&gt; adds ML-based cleaning that removes stains, fingers, and shadows), and &lt;code&gt;androidGalleryImportAllowed&lt;/code&gt; lets users import an existing photo. iOS applies its cleaning automatically and keeps its UI fixed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pageLimit&lt;/code&gt; is enforced by the scanner on Android; on iOS it truncates the result after scanning, because VisionKit exposes no public stop API — and using a private one would risk App Review.&lt;/li&gt;
&lt;li&gt;The Android scanner ships as an on-demand Google Play services module, downloaded on first use, so it adds nothing to your app size.&lt;/li&gt;
&lt;li&gt;Files land in the cache directory with automatic stale-file cleanup — copy anything you want to keep.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin is built exclusively on public platform APIs, is part of &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt;, and requires Capacitor 8+. The &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-document-scanner-plugin/" rel="noopener noreferrer"&gt;full announcement&lt;/a&gt; covers the availability check semantics and the companion plugins for viewing, printing, and sharing the results.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Background Geolocation in Capacitor Without Losing Positions</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:03:42 +0000</pubDate>
      <link>https://dev.to/capawesome/background-geolocation-in-capacitor-without-losing-positions-4k85</link>
      <guid>https://dev.to/capawesome/background-geolocation-in-capacitor-without-losing-positions-4k85</guid>
      <description>&lt;p&gt;Most background location setups in Capacitor apps share a silent failure mode: positions are collected in JavaScript, and JavaScript stops the moment the operating system suspends the web view. The result is a track with holes, discovered in production. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/background-geolocation/" rel="noopener noreferrer"&gt;Capacitor Background Geolocation plugin&lt;/a&gt; to fix exactly that: every position is recorded natively into an SQLite queue and uploaded to your server, whether or not the web view is awake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions the way the platforms actually work
&lt;/h2&gt;

&lt;p&gt;Neither platform lets you ask for foreground and background location in one prompt, so the plugin doesn't pretend otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-background-geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backgroundLocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Watch sessions with real tuning knobs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-background-geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startWatching&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Accuracy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;High&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;distanceFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;androidNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Location Tracking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your location is being tracked.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;androidNotification&lt;/code&gt; is mandatory because Android requires a foreground service with a persistent notification — the plugin makes that explicit instead of inventing one. There's deliberately no accelerometer-driven motion-detection state machine; battery is controlled through predictable knobs (&lt;code&gt;accuracy&lt;/code&gt;, &lt;code&gt;distanceFilter&lt;/code&gt;, &lt;code&gt;androidInterval&lt;/code&gt;, &lt;code&gt;iosPausesAutomatically&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is the source of truth
&lt;/h2&gt;

&lt;p&gt;Enable it once and every position is written natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;positionChange&lt;/code&gt; event becomes what it honestly is — a live feed for your UI — while the queue survives restarts and force-quits. Drain it with &lt;code&gt;getQueuedPositions(...)&lt;/code&gt; and acknowledge with &lt;code&gt;deleteQueuedPositions({ upToId })&lt;/code&gt;; ids are strictly increasing, so a crash between reading and persisting never loses a position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload without JavaScript
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;url&lt;/code&gt; and the queue uploads itself in batches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BackgroundGeolocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/positions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;flushInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer ...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delivery is at least once with a documented status-code contract: &lt;code&gt;2xx&lt;/code&gt; acknowledges, &lt;code&gt;401&lt;/code&gt;/&lt;code&gt;408&lt;/code&gt;/&lt;code&gt;429&lt;/code&gt;/&lt;code&gt;5xx&lt;/code&gt; retry with exponential backoff, anything else drops the batch permanently so it can't block the queue. Deduplicate by position &lt;code&gt;id&lt;/code&gt; on the server and delivery is effectively exactly-once. You can test the whole pipeline against the free &lt;a href="https://background-geolocation-playground.capawesome.io" rel="noopener noreferrer"&gt;Background Geolocation Playground&lt;/a&gt; before your endpoint exists.&lt;/p&gt;

&lt;p&gt;One honest limit: a force-quit ends the watch session — that's an OS restriction. Queued positions survive and upload on the next start, and OS-managed geofencing (a separate plugin) covers relaunch scenarios.&lt;/p&gt;

&lt;p&gt;The plugin is part of &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; and requires Capacitor 8+. The &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-background-geolocation-plugin/" rel="noopener noreferrer"&gt;full announcement&lt;/a&gt; covers the complete server contract and the battery trade-offs.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Fixing the Invisible Camera Preview in Capacitor Barcode Scanners</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:00:55 +0000</pubDate>
      <link>https://dev.to/capawesome/fixing-the-invisible-camera-preview-in-capacitor-barcode-scanners-4dd7</link>
      <guid>https://dev.to/capawesome/fixing-the-invisible-camera-preview-in-capacitor-barcode-scanners-4dd7</guid>
      <description>&lt;p&gt;Search any Capacitor forum for barcode scanning and one bug dominates: the camera preview is invisible. The classic approach renders the camera behind the web view and requires your entire app to be transparent — which works until an Ionic modal, a page transition, or a dark theme paints a background over it.&lt;/p&gt;

&lt;p&gt;We took a different route with the embedded mode of the &lt;a href="https://capawesome.io/docs/sdks/capacitor/barcode-scanner/" rel="noopener noreferrer"&gt;Capacitor Barcode Scanner plugin&lt;/a&gt;: the camera preview is a native view positioned inside your app layout, &lt;strong&gt;above&lt;/strong&gt; the web view, so HTML can never cover it by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Position the camera with a placeholder element
&lt;/h2&gt;

&lt;p&gt;Your markup reserves the space, and the element's bounding rectangle becomes the frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LensFacing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-barcode-scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getScanFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#scanner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;barcodesScanned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Scanned barcodes:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;barcodes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startScan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getScanFrame&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;lensFacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LensFacing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Back&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detected barcodes stream in continuously until &lt;code&gt;stopScan()&lt;/code&gt;. A &lt;code&gt;duplicateTimeout&lt;/code&gt; (default 1500 ms) prevents the same barcode from flooding your handler, &lt;code&gt;formats&lt;/code&gt; restricts detection to what you expect, and &lt;code&gt;detectionArea&lt;/code&gt; limits detection to a region within the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the frame in sync
&lt;/h2&gt;

&lt;p&gt;The native view doesn't reflow with your CSS, so update it when the layout changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;BarcodeScanner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setScanFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getScanFrame&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Overlays are an explicit opt-in
&lt;/h2&gt;

&lt;p&gt;If your design needs HTML drawn over the camera (a viewfinder, detection markers), set &lt;code&gt;placement: PreviewPlacement.Behind&lt;/code&gt; and the preview renders behind the web view. That mode has the same transparency requirement as the classic approach — but now it's a scoped choice for one screen you design around, not a global precondition for scanning at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  It works on the web, too
&lt;/h2&gt;

&lt;p&gt;The embedded mode and &lt;code&gt;readBarcodesFromImage(...)&lt;/code&gt; are supported on the web via the &lt;code&gt;BarcodeDetector&lt;/code&gt; API (with a recommended polyfill for browsers without it). Torch, zoom, and the ready-made fullscreen scanner are native-only.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://capawesome.io/blog/capacitor-embedded-barcode-scanner/" rel="noopener noreferrer"&gt;full guide&lt;/a&gt; also covers torch and zoom control, camera selection, the themeable fullscreen scanner, and migrating from the ML Kit Barcode Scanning plugin.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>webdev</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Announcing the Capacitor MapLibre Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:00:56 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-maplibre-plugin-4hmi</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-maplibre-plugin-4hmi</guid>
      <description>&lt;p&gt;Showing a native map in a Capacitor app has practically meant one thing: Google Maps, plus the billing account it requires before the first tile loads. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/maplibre/" rel="noopener noreferrer"&gt;Capacitor MapLibre plugin&lt;/a&gt;, the first native &lt;a href="https://maplibre.org/" rel="noopener noreferrer"&gt;MapLibre&lt;/a&gt; integration for Capacitor — free, open stack, and no vendor account required by the plugin.&lt;/p&gt;

&lt;p&gt;It renders maps with the native MapLibre SDKs on Android and iOS and with MapLibre GL JS on the Web, so one TypeScript API covers all three platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Native Rendering Works
&lt;/h2&gt;

&lt;p&gt;On Android and iOS, the map is a native view rendered behind the web view, positioned by an empty element in your DOM. The contract is small: the map element stays empty, and it and every ancestor covering the map region use &lt;code&gt;background: transparent&lt;/code&gt; — otherwise the web view paints over the map.&lt;/p&gt;

&lt;p&gt;The upside of this architecture: any DOM element that isn't an ancestor of the map element renders above the map. Floating action buttons, bottom sheets, and dialogs just work. The plugin also keeps the native view in sync with the element's position and size automatically, including while the page scrolls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a Map
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome/capacitor-maplibre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.137154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;11.576124&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;elementId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;styleUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://basemaps.cartocdn.com/gl/positron-gl-style/style.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every method takes the &lt;code&gt;mapId&lt;/code&gt;, so multiple maps can run at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markers with Animated Updates
&lt;/h2&gt;

&lt;p&gt;Markers take custom icons with configurable anchor, size, and rotation, and can be moved with a smooth animation — the building block for live vehicle tracking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MarkerIconAnchor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome/capacitor-maplibre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addAndMoveMarker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMarker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.137154&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;11.576124&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;iconAnchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarkerIconAnchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;iconSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;iconUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/marker.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-marker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMarkerById&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;animationDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;48.370545&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;10.89779&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;markerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-marker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GeoJSON Layers
&lt;/h2&gt;

&lt;p&gt;For routes, areas, and anything your backend already stores as GeoJSON, add a source and style it with line, fill, or circle layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LayerType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome/capacitor-maplibre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addGeoJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGeoJsonSource&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/routes.geojson&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addLayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;layerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lineColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#3887be&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lineWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LayerType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  User Location
&lt;/h2&gt;

&lt;p&gt;Request the location permission, then display and follow the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserTrackingMode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome/capacitor-maplibre&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;enableUserLocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;granted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;MapLibre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableUserLocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;mapId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-map&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trackingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTrackingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Follow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Map Styles and API Keys
&lt;/h2&gt;

&lt;p&gt;The plugin renders any style following the &lt;a href="https://maplibre.org/maplibre-style-spec/" rel="noopener noreferrer"&gt;MapLibre Style Spec&lt;/a&gt;. "No API key required by the plugin" is not the same claim as "free tiles": the style decides where tiles come from. Free providers such as &lt;a href="https://openfreemap.org/" rel="noopener noreferrer"&gt;OpenFreeMap&lt;/a&gt; and &lt;a href="https://github.com/CartoDB/basemap-styles" rel="noopener noreferrer"&gt;CARTO basemaps&lt;/a&gt; exist, while commercial providers such as MapTiler use their own keys. Follow your provider's attribution requirements, and don't ship the default demo style — it's for testing only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The plugin is free, requires Capacitor 8 or later, and one known limitation applies: offline tile management is not part of this version. The full announcement is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-maplibre-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor MapLibre Plugin&lt;/a&gt;, and the &lt;a href="https://capawesome.io/docs/sdks/capacitor/maplibre/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; covers the complete API.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Announcing the Capacitor File Transfer Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:00:49 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-file-transfer-plugin-1mi5</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-file-transfer-plugin-1mi5</guid>
      <description>&lt;p&gt;If you've ever shipped a download feature in a Capacitor app, you know the failure modes: the user switches apps halfway through a 500 MB file, the operating system kills the process, or a big transfer quietly eats someone's mobile data plan. The official &lt;code&gt;@capacitor/file-transfer&lt;/code&gt; plugin covers foreground transfers; we just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-transfer/" rel="noopener noreferrer"&gt;Capacitor File Transfer plugin&lt;/a&gt; to handle everything around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfers as Tasks
&lt;/h2&gt;

&lt;p&gt;The core idea: a transfer is a first-class, persisted object. Starting one returns an identifier immediately, the work runs in native code that outlives your web view (a background &lt;code&gt;URLSession&lt;/code&gt; on iOS, a &lt;code&gt;dataSync&lt;/code&gt; foreground service on Android), and the state can be queried later — even after an app restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start a Download
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-file-transfer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startDownload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startDownload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/file.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/destination/file.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unmetered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;androidNotification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Downloading file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The file is being downloaded.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;network: 'unmetered'&lt;/code&gt; keeps the download off mobile data, and &lt;code&gt;maxRetries&lt;/code&gt; retries with backoff on network errors instead of failing on the first dropped packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uploads, Including S3 Presigned URLs
&lt;/h2&gt;

&lt;p&gt;Uploads send &lt;code&gt;multipart/form-data&lt;/code&gt; by default and switch to a raw binary body for presigned URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-file-transfer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadToPresignedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startUpload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/presigned-url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/source/file.jpg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;uploadType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;binary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file streams straight from the file system into the request — no base64 detour, no blob in JavaScript memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events That Survive Backgrounding
&lt;/h2&gt;

&lt;p&gt;Transfers report their state through &lt;code&gt;transferProgress&lt;/code&gt;, &lt;code&gt;transferCompleted&lt;/code&gt;, and &lt;code&gt;transferFailed&lt;/code&gt; events. The important detail: completed and failed events that occur while no listener is registered are retained and delivered as soon as a listener is added, so a download that finishes while your app is in the background still reaches your code on the next launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pause, Resume, Restore
&lt;/h2&gt;

&lt;p&gt;Downloads can be paused and resumed at any time — even after the process was killed, via resume data on iOS and HTTP &lt;code&gt;Range&lt;/code&gt; requests on Android. And because transfers are persisted, &lt;code&gt;getTransfers()&lt;/code&gt; rebuilds a download manager UI after a restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-file-transfer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;restoreTransferList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transfers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTransfers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paused&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uploads cannot be paused: plain HTTP uploads have no standard resume mechanism, so the plugin rejects instead of faking a pause that would restart from zero anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens in Each App State
&lt;/h2&gt;

&lt;p&gt;Instead of leaving it to experimentation, the behavior is documented per platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App state&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foreground&lt;/td&gt;
&lt;td&gt;Runs.&lt;/td&gt;
&lt;td&gt;Runs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgrounded&lt;/td&gt;
&lt;td&gt;Runs in the &lt;code&gt;dataSync&lt;/code&gt; foreground service.&lt;/td&gt;
&lt;td&gt;Runs in the background &lt;code&gt;URLSession&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Killed by the OS&lt;/td&gt;
&lt;td&gt;Interrupted; restored as &lt;code&gt;failed&lt;/code&gt;, downloads resumable.&lt;/td&gt;
&lt;td&gt;Continued by the OS and delivered on relaunch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force-quit by the user&lt;/td&gt;
&lt;td&gt;Interrupted; restored as &lt;code&gt;failed&lt;/code&gt;, downloads resumable.&lt;/td&gt;
&lt;td&gt;Canceled by the OS (documented iOS behavior).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Resuming an interrupted download requires the server to support the HTTP &lt;code&gt;Range&lt;/code&gt; header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from @capacitor/file-transfer
&lt;/h2&gt;

&lt;p&gt;The switch is mostly a rename plus a change of model — transfers become asynchronous tasks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;@capacitor/file-transfer&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;@capawesome-team/capacitor-file-transfer&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;downloadFile({ url, path })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startDownload({ url, path })&lt;/code&gt;, resolves with &lt;code&gt;{ id }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uploadFile({ url, path })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startUpload({ url, path })&lt;/code&gt;, resolves with &lt;code&gt;{ id }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addListener('progress', ...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;addListener('transferProgress', ...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transferCompleted&lt;/code&gt; and &lt;code&gt;transferFailed&lt;/code&gt; events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pauseTransferById&lt;/code&gt;, &lt;code&gt;resumeTransferById&lt;/code&gt;, &lt;code&gt;cancelTransferById&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No equivalent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getTransferById&lt;/code&gt;, &lt;code&gt;getTransfers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The plugin is part of the &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; subscription and requires Capacitor 8 or later. The full announcement is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-file-transfer-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor File Transfer Plugin&lt;/a&gt;, and the &lt;a href="https://capawesome.io/docs/sdks/capacitor/file-transfer/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; covers the complete API.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Announcing the Capacitor Health Plugin</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 19:30:07 +0000</pubDate>
      <link>https://dev.to/capawesome/announcing-the-capacitor-health-plugin-m60</link>
      <guid>https://dev.to/capawesome/announcing-the-capacitor-health-plugin-m60</guid>
      <description>&lt;p&gt;Health data on mobile lives in two stores: Apple HealthKit on iOS and Health Connect on Android. If you build a fitness or health app with Capacitor, you normally end up writing two integrations that disagree on permissions, sleep modeling, and even what a "workout" is. We just released the &lt;a href="https://capawesome.io/docs/sdks/capacitor/health/" rel="noopener noreferrer"&gt;Capacitor Health plugin&lt;/a&gt; to solve exactly that: one strictly typed API that reads, writes, and aggregates data from both stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Health Plugin Now
&lt;/h2&gt;

&lt;p&gt;The timing follows the platforms. The Google Fit APIs shut down at the end of 2026, and Health Connect takes over as the health store on Android, while HealthKit has held that role on iOS all along. Any cross-platform health integration you build today should target these two stores — and ideally without maintaining two data models in your app code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregation First
&lt;/h2&gt;

&lt;p&gt;Most health plugins hand you raw record lists and leave the math to you. That breaks the moment a user wears a smartwatch: the watch and the phone both record steps for the same minutes, and summing the records in JavaScript counts that overlap twice.&lt;/p&gt;

&lt;p&gt;Both HealthKit and Health Connect solve this with native aggregation queries that deduplicate sources before returning a number, so the plugin is built around them. A week of daily step totals is one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readDailySteps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;, and &lt;code&gt;month&lt;/code&gt; buckets are calendar-aware and follow the device's time zone. Cumulative data types such as steps, distance, and calories support &lt;code&gt;sum&lt;/code&gt;; sampled types such as heart rate and weight support &lt;code&gt;average&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, and &lt;code&gt;minimum&lt;/code&gt;. An unsupported combination rejects with the &lt;code&gt;INVALID_AGGREGATION&lt;/code&gt; error code instead of resolving with silently empty results, so a mistyped query fails in development instead of shipping as a dashboard full of zeros.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability and Permissions
&lt;/h2&gt;

&lt;p&gt;Health Connect exists in three states on Android: available, not installed, or unsupported by the device. On Android 9 to 13 it's a separate app the user may not have; on Android 14 and later it's part of the operating system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkAvailability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;health-connect-not-installed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;installHealthConnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions are requested per data type and separately for reading and writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestPermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Steps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HeartRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  An Honest Permission Model
&lt;/h2&gt;

&lt;p&gt;Here's a HealthKit detail that surprises most developers: iOS deliberately hides whether a read permission was granted. A denied permission that is distinguishable from missing data would leak sensitive information — an app that knows it was denied blood glucose access could conclude the user is likely diabetic.&lt;/p&gt;

&lt;p&gt;The plugin reports iOS read permissions as &lt;code&gt;prompt&lt;/code&gt; before the first request and &lt;code&gt;unknown&lt;/code&gt; afterwards, never as &lt;code&gt;granted&lt;/code&gt;. Reporting &lt;code&gt;granted&lt;/code&gt; would be an invented value, so the plugin doesn't do it. Design your app around the presence of data: request the permissions, query, and show a helpful empty state when nothing comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading and Writing Records
&lt;/h2&gt;

&lt;p&gt;When you need individual samples instead of aggregates, &lt;code&gt;readRecords()&lt;/code&gt; returns them with timestamps and source, and &lt;code&gt;writeRecord()&lt;/code&gt; logs the record types apps commonly write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readHeartRateSamples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readRecords&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HeartRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logWeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeRecord&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;71.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workouts have their own reader: &lt;code&gt;readWorkouts()&lt;/code&gt; returns exercise sessions with type, duration, and totals, whether logged by your app or another one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing App Review
&lt;/h2&gt;

&lt;p&gt;Health integrations fail review more often than they fail at runtime. Every Android app integrating with Health Connect must complete the Health apps declaration in the Google Play Console and provide a privacy policy; Apple reviews health apps against App Review Guideline 5.1.3. The &lt;a href="https://capawesome.io/docs/sdks/capacitor/health/" rel="noopener noreferrer"&gt;plugin documentation&lt;/a&gt; includes dedicated sections for both, so the policy work is part of the setup instead of a surprise at submission time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;The Capacitor Health plugin covers around 20 data types, requires Capacitor 8 or later, and is available today as part of the &lt;a href="https://capawesome.io/insiders/" rel="noopener noreferrer"&gt;Capawesome Insiders&lt;/a&gt; subscription. The full announcement with more details is on our blog: &lt;a href="https://capawesome.io/blog/announcing-the-capacitor-health-plugin/" rel="noopener noreferrer"&gt;Announcing the Capacitor Health Plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions or feedback? Drop a comment — happy to answer.&lt;/p&gt;

</description>
      <category>ionic</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Google Play Contacts Policy 2027 for Capacitor</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:45:02 +0000</pubDate>
      <link>https://dev.to/capawesome/google-play-contacts-policy-2027-for-capacitor-3ak</link>
      <guid>https://dev.to/capawesome/google-play-contacts-policy-2027-for-capacitor-3ak</guid>
      <description>&lt;p&gt;Google Play's new Contacts Permissions policy takes effect on January 27, 2027. Apps that target Android 17 (API level 37) or later may only declare &lt;code&gt;READ_CONTACTS&lt;/code&gt; if the Android Contact Picker cannot cover their core functionality. If your Capacitor app only lets users pick a contact, you need to drop the permission, and with the &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/" rel="noopener noreferrer"&gt;Capacitor Contacts plugin&lt;/a&gt; that means passing the &lt;code&gt;property&lt;/code&gt; option to &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is a catch that Google's announcement doesn't mention. Switching to the system contact picker does not by itself free you from &lt;code&gt;READ_CONTACTS&lt;/code&gt;. Below Android 17, the picker hands your app an access grant so narrow that it contains no phone number, no email address, and no structured name (only a display name). This guide covers what the policy requires, why the picker alone falls short, and how to select a contact detail without ever asking for the permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Google Play's &lt;a href="https://support.google.com/googleplay/android-developer/answer/16926792" rel="noopener noreferrer"&gt;Contacts Permissions policy&lt;/a&gt; was announced on April 15, 2026 and becomes effective on January 27, 2027.&lt;/li&gt;
&lt;li&gt;It applies to apps that target Android 17 (API level 37) or later. Those apps may only request &lt;code&gt;READ_CONTACTS&lt;/code&gt; if the Android Contact Picker is not sufficient for their core functionality.&lt;/li&gt;
&lt;li&gt;Apps that still need broad access must submit a Play Console declaration naming the features that require it and explaining why the picker falls short.&lt;/li&gt;
&lt;li&gt;The contact picker grants read access to the picked contact URI only. That URI exposes no phone numbers, email addresses, or structured name, so reading contact details still requires &lt;code&gt;READ_CONTACTS&lt;/code&gt; below Android 17.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;property&lt;/code&gt; option of &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; lets the user select a single phone number, email address, or postal address. It requires no permission on any Android version and is available since version 8.1.0 of the Capacitor Contacts plugin.&lt;/li&gt;
&lt;li&gt;On iOS, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; has never required a permission.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Does Google Play's Contacts Permissions Policy Require?
&lt;/h2&gt;

&lt;p&gt;The policy reserves &lt;code&gt;READ_CONTACTS&lt;/code&gt; for apps that genuinely cannot work without the full address book. Google's wording is that apps which don't need broad access "must use the Android Contact Picker, a more secure, easy-to-integrate alternative that minimizes data collection and improves user safety."&lt;/p&gt;

&lt;p&gt;Three details decide whether this affects you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who it applies to.&lt;/strong&gt; Only apps that target Android 17 (API level 37) or later. Since Google Play raises the required target API level every year, that will be every actively maintained app soon enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it lands.&lt;/strong&gt; Google &lt;a href="https://support.google.com/googleplay/android-developer/answer/16926792" rel="noopener noreferrer"&gt;announced the policy&lt;/a&gt; on April 15, 2026 and set the effective date to January 27, 2027. Pre-review checks in the Play Console start on October 27, 2026, so you will see warnings before enforcement begins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the escape hatch costs.&lt;/strong&gt; Apps that need ongoing access to the whole contact list keep it, but they have to file a &lt;a href="https://support.google.com/googleplay/android-developer/answer/16935362" rel="noopener noreferrer"&gt;Play Developer Declaration&lt;/a&gt; that names the user-facing feature and explains why the picker is technically insufficient. Automatic 30-day extensions are available through the Play Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A messaging app that syncs your address book to find friends has a case to make. A checkout screen that fills in a delivery address does not, and that second group is where most Capacitor apps sit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does the Contact Picker Still Need READ_CONTACTS?
&lt;/h2&gt;

&lt;p&gt;Because the permission grant the picker returns is narrower than the data you asked for. When the user selects someone, the system grants your app read access to the picked contact URI, and that grant is exact: it covers that one URI and nothing below or beside it.&lt;/p&gt;

&lt;p&gt;The problem is what lives at that URI. A row in the &lt;code&gt;Contacts&lt;/code&gt; table holds an identifier and some metadata. It holds no phone numbers, no email addresses, and no structured name, because in Android's contacts model those live in the separate &lt;code&gt;ContactsContract.Data&lt;/code&gt; table. Querying that table is a global read, and the grant does not extend to it. It does not extend to the contact's &lt;code&gt;entities&lt;/code&gt; sub-directory either. Attempt it without the permission and the provider answers with a flat refusal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permission Denial: reading ContactsProvider2 uri content://com.android.contacts/contacts/1/entities requires android.permission.READ_CONTACTS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "use the picker instead of the permission" is only half an instruction below Android 17. The picker gives you a contact you are allowed to identify but not allowed to read. There is no permission-free way to pull a whole contact record on those versions, which is why the plugin needs a different approach rather than a different intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Pick a Contact Without the READ_CONTACTS Permission
&lt;/h2&gt;

&lt;p&gt;Ask the picker for a single contact property instead of a whole contact. The &lt;code&gt;property&lt;/code&gt; option of &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; launches the picker against the phone, email, or postal address table directly, so the user selects one specific value rather than a person:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Contacts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@capawesome-team/capacitor-contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pickPhoneNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickContacts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;phoneNumbers&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because the picker now returns a data row URI, and the access it grants points at the row that actually holds the value. Your app reads it directly, on every Android version, without a permission in the manifest and without a runtime prompt. To install the Capacitor Contacts plugin, please refer to the &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#installation" rel="noopener noreferrer"&gt;Installation&lt;/a&gt; section in the plugin documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#contactproperty" rel="noopener noreferrer"&gt;&lt;code&gt;ContactProperty&lt;/code&gt;&lt;/a&gt; offers three values, matching the three data tables the system picker can target:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Selects&lt;/th&gt;
&lt;th&gt;Read from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.PhoneNumber&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single phone number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;phoneNumbers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.EmailAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single email address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;emailAddresses&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContactProperty.PostalAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A single postal address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postalAddresses&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result is deliberately thin. You get the contact &lt;code&gt;id&lt;/code&gt;, the new &lt;code&gt;displayName&lt;/code&gt; property, and the property the user picked, and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Contacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pickContacts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ContactProperty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EmailAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'John Doe'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailAddresses&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 'john.doe@example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;displayName&lt;/code&gt; is a read-only property added in version 8.1.0. It holds the formatted name the device itself shows for the contact, derived from &lt;code&gt;CNContactFormatter&lt;/code&gt; on iOS and &lt;code&gt;Data.DISPLAY_NAME&lt;/code&gt; on Android, and it is the only name the granted URIs expose. Setting it when creating or updating a contact has no effect, so keep using &lt;code&gt;givenName&lt;/code&gt; and &lt;code&gt;familyName&lt;/code&gt; for that.&lt;/p&gt;

&lt;p&gt;Practically, this changes how you design the interaction. Instead of one "Choose a contact" button followed by a disambiguation dialog when someone has four phone numbers, you send the user straight into a picker that lists the numbers. Fewer taps for them, no permission prompt for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes on Android 17?
&lt;/h2&gt;

&lt;p&gt;Android 17 makes plain contact picking permission-free on its own. Apps targeting API level 37 get their &lt;code&gt;ACTION_PICK&lt;/code&gt; intent automatically upgraded to the new system contact picker, which returns a picker session URI following the &lt;code&gt;ContactsContract.Data&lt;/code&gt; schema. The plugin detects that URI and reads it directly, so a call to &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; without the &lt;code&gt;property&lt;/code&gt; option also stops needing &lt;code&gt;READ_CONTACTS&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;That does not make the &lt;code&gt;property&lt;/code&gt; option redundant. Your app still runs on Android 16 and below, where the old behavior applies, and you cannot ship a manifest that declares &lt;code&gt;READ_CONTACTS&lt;/code&gt; on old devices but not on new ones. As long as you support anything below Android 17, the &lt;code&gt;property&lt;/code&gt; option is what lets you leave the permission out entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Do You Still Need READ_CONTACTS?
&lt;/h2&gt;

&lt;p&gt;Whenever your app reads the address book without the user pointing at a specific entry. Every method that queries contacts on its own terms falls in this group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;getContacts(...)&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getcontactbyid" rel="noopener noreferrer"&gt;&lt;code&gt;getContactById(...)&lt;/code&gt;&lt;/a&gt;, which read the address book directly.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#countcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;countContacts()&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getgroups" rel="noopener noreferrer"&gt;&lt;code&gt;getGroups()&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#getaccounts" rel="noopener noreferrer"&gt;&lt;code&gt;getAccounts()&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Anything that syncs, backs up, or matches the full contact list against a server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app does one of these as a core feature, the policy does not shut you out. Keep the permission and file the declaration. What the policy targets is the app that declares &lt;code&gt;READ_CONTACTS&lt;/code&gt; to power a single "pick a friend" screen, and that app now has a cheaper option.&lt;/p&gt;

&lt;p&gt;Two methods need no permission at all and are worth knowing about: &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#displaycreatecontact" rel="noopener noreferrer"&gt;&lt;code&gt;displayCreateContact(...)&lt;/code&gt;&lt;/a&gt; hands the whole creation flow to the system UI, and on iOS &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; has always run without one, because &lt;code&gt;CNContactPickerViewController&lt;/code&gt; returns the selected contact to the app without touching the contacts entitlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Migrate Your Capacitor App
&lt;/h2&gt;

&lt;p&gt;Work through it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every contacts call in your codebase.&lt;/strong&gt; Search for &lt;code&gt;Contacts.&lt;/code&gt; and sort the hits into two buckets: user-driven selection, and everything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite the selection calls.&lt;/strong&gt; Replace &lt;code&gt;pickContacts()&lt;/code&gt; with a &lt;code&gt;property&lt;/code&gt; variant and adjust the code that consumes the result, since it now receives one value instead of a full contact object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide about the rest.&lt;/strong&gt; If the second bucket is empty, you are done and the permission can go. If it isn't, check whether those features are genuinely core to your app or leftovers you can drop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the permission.&lt;/strong&gt; Delete `&lt;code&gt;from your&lt;/code&gt;AndroidManifest.xml&lt;code&gt;. Leave&lt;/code&gt;WRITE_CONTACTS` alone if you create or update contacts, as this policy does not cover it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on a real device.&lt;/strong&gt; Build with the permission removed and run every flow that touches contacts. A missing grant surfaces as a &lt;code&gt;Permission Denial&lt;/code&gt; in Logcat, not as a friendly error, so watch the log while you click through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File the declaration if you kept the permission.&lt;/strong&gt; Do it before pre-review checks start on October 27, 2026 rather than in the week before the January deadline.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When does Google Play's Contacts Permissions policy take effect?
&lt;/h3&gt;

&lt;p&gt;January 27, 2027. Google announced it on April 15, 2026, and pre-review checks in the Play Console begin on October 27, 2026, which gives you roughly three months of warnings before enforcement. Automatic 30-day extensions can be requested through the Play Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the policy apply if my app targets Android 16?
&lt;/h3&gt;

&lt;p&gt;Not yet. The policy covers apps that target Android 17 (API level 37) or later. Google Play raises the minimum target API level for updates every year, though, so an app that is still maintained will reach API 37 on its own schedule. Migrating early costs less than migrating under a deadline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this affect my app on iOS?
&lt;/h3&gt;

&lt;p&gt;No. This is a Google Play policy and applies to Android only. On iOS, &lt;a href="https://capawesome.io/docs/sdks/capacitor/contacts/#pickcontacts" rel="noopener noreferrer"&gt;&lt;code&gt;pickContacts(...)&lt;/code&gt;&lt;/a&gt; never required the contacts permission, and the &lt;code&gt;property&lt;/code&gt; option behaves the same way there, returning the &lt;code&gt;id&lt;/code&gt;, the &lt;code&gt;displayName&lt;/code&gt;, and the selected property.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I still read a full contact after the user picks one?
&lt;/h3&gt;

&lt;p&gt;Only with &lt;code&gt;READ_CONTACTS&lt;/code&gt; below Android 17. The picker's grant covers the picked contact URI, which carries no phone numbers, email addresses, or structured name, and reading those means querying the &lt;code&gt;ContactsContract.Data&lt;/code&gt; table, which the grant does not cover. From Android 17 on, the upgraded system picker returns a data-schema URI that the plugin reads without the permission.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I keep READ_CONTACTS without filing a declaration?
&lt;/h3&gt;

&lt;p&gt;Once the policy is effective, apps targeting Android 17 or later that declare the permission without an approved declaration are subject to enforcement, which in practice means your app updates get blocked in the Play Console. The declaration itself asks which user-facing features need the permission and why the Android Contact Picker is technically insufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the property option work on older Android versions?
&lt;/h3&gt;

&lt;p&gt;Yes. Picking against the phone, email, or postal address table is not an Android 17 feature. The picker returns a data row URI it grants access to on every supported Android version, which is exactly what makes the option a safe way to remove the permission from your manifest today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The migration is smaller than the policy makes it sound. For most Capacitor apps it comes down to one option on one method call and one line deleted from the Android manifest. What takes the thinking is the audit: knowing which of your contacts calls are user-driven selection and which ones read the address book on their own, because only the second group needs a declaration.&lt;/p&gt;

&lt;p&gt;For a full tour of the plugin's API, from permissions to accounts and groups, read &lt;a href="https://capawesome.io/blog/exploring-the-capacitor-contacts-api/" rel="noopener noreferrer"&gt;Exploring the Capacitor Contacts API&lt;/a&gt;. If you have questions, join the &lt;a href="https://discord.gg/VCXxSVjefW" rel="noopener noreferrer"&gt;Capawesome Discord server&lt;/a&gt;, and subscribe to the &lt;a href="https://capawesome.io/newsletter/" rel="noopener noreferrer"&gt;Capawesome newsletter&lt;/a&gt; to stay up to date with new plugins and guides.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>ionic</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
