<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[wafrat]]></title><description><![CDATA[Flutter enthusiast, ex-Google engineer living in Seoul. Are you living in or visiting Korea? Let's chat!]]></description><link>https://www.wafrat.com/</link><image><url>https://www.wafrat.com/favicon.png</url><title>wafrat</title><link>https://www.wafrat.com/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Wed, 29 Apr 2026 10:01:09 GMT</lastBuildDate><atom:link href="https://www.wafrat.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Using SharedPreferences in tests]]></title><description><![CDATA[<p><a href="https://pub.dev/packages/shared_preferences">SharedPreferences</a> actually provides an in-memory implementation, but it is not documented in the README. If you don&apos;t set it up, SharedPreferences throws a warning when it&apos;s used in tests:</p><blockquote>MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)</blockquote><p>The solution is</p>]]></description><link>https://www.wafrat.com/using-sharedpreference-in-tests/</link><guid isPermaLink="false">6959e150021ca2165983d5da</guid><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Sun, 04 Jan 2026 03:45:54 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://pub.dev/packages/shared_preferences">SharedPreferences</a> actually provides an in-memory implementation, but it is not documented in the README. If you don&apos;t set it up, SharedPreferences throws a warning when it&apos;s used in tests:</p><blockquote>MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)</blockquote><p>The solution is to run <code>SharedPreferences.setMockInitialValues({ values })</code>.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/documentation/shared_preferences/latest/shared_preferences/SharedPreferences/setMockInitialValues.html"><div class="kg-bookmark-content"><div class="kg-bookmark-title">setMockInitialValues method - SharedPreferences class - shared_preferences library - Dart API</div><div class="kg-bookmark-description">API docs for the setMockInitialValues method from the SharedPreferences class, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart API</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-91ajgdgb/img/dart-logo.svg" alt></div></a></figure><p>I wish it was prominently shown in the package docs.</p><p>I did find a mention of it here: <a href="https://docs.flutter.dev/cookbook/persistence/key-value#testing-support">https://docs.flutter.dev/cookbook/persistence/key-value#testing-support</a></p><p><a href="https://github.com/flutter/flutter/issues/153108#issuecomment-2286996765">This ticket</a> seems to say the method is deprecated, but as of today, it runs fine.</p>]]></content:encoded></item><item><title><![CDATA[Mocking http.Client (updated)]]></title><description><![CDATA[<p>In the <a href="https://www.wafrat.com/mocking-http-client/">previous post</a> from 2022, we already saw that we can use Mockito with <code>@GenerateNiceMocks</code> to generate a <code>MockClient</code>, or use http&apos;s own <code>MockClient</code> and pass it to classes. But what if the code is from a package that does not accept a <code>Client</code>?</p><h1 id="investigation">Investigation</h1><p>In that</p>]]></description><link>https://www.wafrat.com/mocking-http-client-2/</link><guid isPermaLink="false">6959ca69021ca2165983d56e</guid><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Sun, 04 Jan 2026 02:26:40 GMT</pubDate><content:encoded><![CDATA[<p>In the <a href="https://www.wafrat.com/mocking-http-client/">previous post</a> from 2022, we already saw that we can use Mockito with <code>@GenerateNiceMocks</code> to generate a <code>MockClient</code>, or use http&apos;s own <code>MockClient</code> and pass it to classes. But what if the code is from a package that does not accept a <code>Client</code>?</p><h1 id="investigation">Investigation</h1><p>In that case we can&apos;t easily pass a mock <code>Client</code>. Let&apos;s look at the code that will run with the test:</p><pre><code class="language-dart">var response = await http.get(Uri.parse(url), headers: allHeaders);</code></pre><p>If we dig into http&apos;s code, we see:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">Future&lt;Response&gt; get(Uri url, {Map&lt;String, String&gt;? headers}) =&gt;
    _withClient((client) =&gt; client.get(url, headers: headers));


Future&lt;T&gt; _withClient&lt;T&gt;(Future&lt;T&gt; Function(Client) fn) async {
  var client = Client();
  try {
    return await fn(client);
  } finally {
    client.close();
  }
}


abstract interface class Client {
  /// Creates a new platform appropriate client.
  ///
  /// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if
  /// `dart:js_interop` is available, otherwise it will throw an unsupported
  /// error.
  factory Client() =&gt; zoneClient ?? createClient();

  ...
}

@internal
Client? get zoneClient {
  final client = Zone.current[#_clientToken];
  return client == null ? null : (client as Client Function())();
}


/// Runs [body] in its own [Zone] with the [Client] returned by [clientFactory]
/// set as the default [Client].
/// [...]
/// &gt; [!IMPORTANT]
/// &gt; Flutter does not guarantee that callbacks are executed in a particular
/// &gt; [Zone].
/// &gt;
/// &gt; Instead of using [runWithClient], Flutter developers can use a framework,
/// &gt; such as [`package:provider`](https://pub.dev/packages/provider), to make
/// &gt; a [Client] available throughout their applications.
/// &gt;
/// &gt; See the
/// &gt; [Flutter Http Example](https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example).
R runWithClient&lt;R&gt;(R Function() body, Client Function() clientFactory,
        {ZoneSpecification? zoneSpecification}) =&gt;
    runZoned(body,
        zoneValues: {#_clientToken: Zone.current.bindCallback(clientFactory)},
        zoneSpecification: zoneSpecification);
</code></pre><figcaption>http</figcaption></figure><h1 id="runwithclient"><code>runWithClient</code></h1><p>Here&apos;s how I used <code>runWithClient</code> to pass my MockClient:</p><pre><code class="language-dart">await http.runWithClient(
  () async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Provider&lt;FirebaseAuth&gt;(
            create: (_) =&gt; MockFirebaseAuth(),
            child: Provider&lt;ChatService&gt;(
              create: (_) =&gt; MockChatService(),
              child: BubbleWidget(b),
            ),
          ),
        ),
      ),
    );
  },
  () =&gt; MockClient((request) async {
    final content = File(&apos;test/airtag.html&apos;).readAsStringSync();
    return http.Response(
      content,
      200,
      headers: {&apos;content-type&apos;: &apos;text/html&apos;},
    );
  }),
);
</code></pre><h1 id="content-type"><code>content-type</code></h1><p>I can see that my <code>MockClient</code> callback is executed but I get this exception when initializing Response:</p><blockquote><code>[package_name] - Error in https://www.[site].com/[some_article].html response (Invalid argument (string): Contains invalid characters.: &quot;&lt;!DOCTYPE html&gt;&lt;html lang=\&quot;en-US\&quot;&gt;&lt;head&gt;&lt;meta charSet=\&quot;utf-8\&quot;/&gt;</code></blockquote><p>I can see that readAsStringSync() reads in utf-8 by default, so <code>content</code> is fine. Should I add content-type to the headers?</p><p>I tried with <code>headers: {&apos;content-type&apos;: &apos;text/html&apos;, &apos;charset&apos;: &apos;utf-8&apos;}</code>, but it failed too.</p><p>Later when I googled some more, I found a post that set it up like this: <code>headers: {&apos;content-type&apos;: &apos;text/html; charset=utf-8&apos;}</code>. And sure enough, this worked.</p><p>Looking back, if I had carefully read the documentation, I&apos;d have found out myself:</p><figure class="kg-card kg-code-card"><pre><code>/// An HTTP response where the entire response body is known in advance.
class Response extends BaseResponse {
  /// The bytes comprising the body of this response.
  final Uint8List bodyBytes;

  /// The body of the response as a string.
  ///
  /// This is converted from [bodyBytes] using the `charset` parameter of the
  /// `Content-Type` header field, if available. If it&apos;s unavailable or if the
  /// encoding name is unknown:
  /// - [utf8] is used when the content-type is &apos;application/json&apos; (see [RFC 3629][]).
  /// - [latin1] is used in all other cases (see [RFC 2616][])
  ///
  /// [RFC 3629]: https://www.rfc-editor.org/rfc/rfc3629.
  /// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
  String get body =&gt; _encodingForHeaders(headers).decode(bodyBytes);

  /// Creates a new HTTP response with a string body.
  Response(String body, int statusCode,
</code></pre><figcaption>http&apos;s Response.dart</figcaption></figure><p>Here&apos;s the examples from <a href="https://www.rfc-editor.org/rfc/rfc9110.html">https://www.rfc-editor.org/rfc/rfc9110.html</a> (redirected from RFC 2616):</p><pre><code>For example, the following media types are equivalent in describing HTML text data encoded in the UTF-8 character encoding scheme, but the first is preferred for consistency (the &quot;charset&quot; parameter value is defined as being case-insensitive in [RFC2046], Section 4.1.2):

  text/html;charset=utf-8
  Text/HTML;Charset=&quot;utf-8&quot;
  text/html; charset=&quot;utf-8&quot;
  text/html;charset=UTF-8</code></pre><p>Now the test works fine.</p><h1 id="clean-up-with-provider">Clean up with <code>Provider</code>?</h1><p>According to the docs for <code>runWithClient</code>, in Flutter, we can use a <code>Provider</code> instead. This makes the code much cleaner:</p><pre><code class="language-dart">await tester.pumpWidget(
  MaterialApp(
    home: Scaffold(
      body: Provider&lt;http.Client&gt;(
        create: (_) =&gt; MockClient((request) async {
          MockClient((request) async {
            final content = File(&apos;test/airtag.html&apos;).readAsStringSync();
            return http.Response(
              content,
              200,
              headers: {&apos;content-type&apos;: &apos;text/html; charset=utf-8&apos;},
            );
          }),
        }),
        child: BubbleWidget(b),
      ),
    ),
  ),
);
</code></pre><p>Unfortunately, this did not work. I don&apos;t know the system well enough to figure out why. But basically, the Widget that was supposed to use Client and render, seemed to be immediately destroyed?... I don&apos;t even know if that&apos;s possible. I put breakpoints in that Widget&apos;s build and it was never called.</p><p>So I ended up using <code>runWithClient</code>. Maybe some day I&apos;ll figure out how to use <code>Provider</code>.</p>]]></content:encoded></item><item><title><![CDATA[Who's the gourmet brewmaster from Culinary Class Wars 2?]]></title><description><![CDATA[<p>The one who makes soju? <a href="https://www.instagram.com/yunjudang/">https://www.instagram.com/yunjudang/</a>. You&apos;re welcome!</p>]]></description><link>https://www.wafrat.com/whos-the-gourmet-brewmaster-from-culinary-class-wars-2/</link><guid isPermaLink="false">69421b46021ca2165983d55f</guid><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Wed, 17 Dec 2025 02:54:47 GMT</pubDate><content:encoded><![CDATA[<p>The one who makes soju? <a href="https://www.instagram.com/yunjudang/">https://www.instagram.com/yunjudang/</a>. You&apos;re welcome!</p>]]></content:encoded></item><item><title><![CDATA[Mocking cross-platform plugins like url_launcher]]></title><description><![CDATA[<p>Today I wanted to mock <a href="https://pub.dev/packages/url_launcher">url_launcher</a>, but it was not as easy as the docs said.</p><p>Here&apos;s the code I wanted to test:</p><pre><code class="language-dart">Future launchSearch(String word, SearchType searchType) async {
  final url = getSearchUrl(word, searchType);
  Uri uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri)</code></pre>]]></description><link>https://www.wafrat.com/mocking-cross-platform-plugins/</link><guid isPermaLink="false">6940edd9021ca2165983d505</guid><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Tue, 16 Dec 2025 05:42:25 GMT</pubDate><content:encoded><![CDATA[<p>Today I wanted to mock <a href="https://pub.dev/packages/url_launcher">url_launcher</a>, but it was not as easy as the docs said.</p><p>Here&apos;s the code I wanted to test:</p><pre><code class="language-dart">Future launchSearch(String word, SearchType searchType) async {
  final url = getSearchUrl(word, searchType);
  Uri uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
  } else {
    throw &apos;Could not launch $url&apos;;
  }
}</code></pre><p>I wanted to check that it launched the correct URL.</p><p><code>canLaunchUrl</code> and <code>launchUrl</code> use Platform dependent code:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">Future&lt;bool&gt; canLaunchUrl(Uri url) async {
  return UrlLauncherPlatform.instance.canLaunch(url.toString());
}

...

Future&lt;bool&gt; launchUrl(
  Uri url, {
  LaunchMode mode = LaunchMode.platformDefault,
  WebViewConfiguration webViewConfiguration = const WebViewConfiguration(),
  BrowserConfiguration browserConfiguration = const BrowserConfiguration(),
  String? webOnlyWindowName,
}) async {
  if ((mode == LaunchMode.inAppWebView ||
          mode == LaunchMode.inAppBrowserView) &amp;&amp;
      !(url.scheme == &apos;https&apos; || url.scheme == &apos;http&apos;)) {
    throw ArgumentError.value(url, &apos;url&apos;,
        &apos;To use an in-app web view, you must provide an http(s) URL.&apos;);
  }
  return UrlLauncherPlatform.instance.launchUrl(
    url.toString(),
    LaunchOptions(...),
  );
}
</code></pre><figcaption>url_launcher_uri.dart</figcaption></figure><p>And the package lets you set the instance:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">abstract class UrlLauncherPlatform extends PlatformInterface {
  /// Constructs a UrlLauncherPlatform.
  UrlLauncherPlatform() : super(token: _token);

  static final Object _token = Object();

  static UrlLauncherPlatform _instance = MethodChannelUrlLauncher();

  /// The default instance of [UrlLauncherPlatform] to use.
  ///
  /// Defaults to [MethodChannelUrlLauncher].
  static UrlLauncherPlatform get instance =&gt; _instance;

  /// Platform-specific plugins should set this with their own platform-specific
  /// class that extends [UrlLauncherPlatform] when they register themselves.
  // TODO(amirh): Extract common platform interface logic.
  // https://github.com/flutter/flutter/issues/43368
  static set instance(UrlLauncherPlatform instance) {
    PlatformInterface.verify(instance, _token);
    _instance = instance;
  }
  ...
}</code></pre><figcaption>url_launcher_platform_interface&apos;s url_launcher_platform.dart</figcaption></figure><p>And <a href="https://pub.dev/packages/plugin_platform_interface">plugin_platform_interface</a>&apos;s code explains how to override it in tests:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">  /// Ensures that the platform instance was constructed with a non-`const` token
  /// that matches the provided token and throws [AssertionError] if not.
  ///
  /// This is used to ensure that implementers are using `extends` rather than
  /// `implements`.
  ///
  /// Subclasses of [MockPlatformInterfaceMixin] are assumed to be valid in debug
  /// builds.
  ///
  /// This is implemented as a static method so that it cannot be overridden
  /// with `noSuchMethod`.
  static void verify(PlatformInterface instance, Object token) {
    _verify(instance, token, preventConstObject: true);
  }
</code></pre><figcaption>plugin_platform_interface.dart</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-dart">/// A [PlatformInterface] mixin that can be combined with fake or mock objects,
/// such as test&apos;s `Fake` or mockito&apos;s `Mock`.
///
/// It passes the [PlatformInterface.verify] check even though it isn&apos;t
/// using `extends`.
///
/// This class is intended for use in tests only.
///
/// Sample usage (assuming `UrlLauncherPlatform` extends [PlatformInterface]):
///
/// ```dart
/// class UrlLauncherPlatformMock extends Mock
///    with MockPlatformInterfaceMixin
///    implements UrlLauncherPlatform {}
/// ```
@visibleForTesting
abstract mixin class MockPlatformInterfaceMixin implements PlatformInterface {}
</code></pre><figcaption>plugin_platform_interface.dart</figcaption></figure><p>Interesting that plugin_platform_interface shows the sample code for the exact same package I am trying to mock!</p><p>So I copy/paste this code, and try it in my tests:</p><pre><code class="language-dart">main() {
  testWidgets(&apos;Search menu icon&apos;, (WidgetTester tester) async {
    // Set up UrlLauncher mocks.
    final urlLauncherPlatformMock = UrlLauncherPlatformMock();
    UrlLauncherPlatform.instance = urlLauncherPlatformMock;
    when(urlLauncherPlatformMock.canLaunch(any))
        .thenAnswer((_) async =&gt; Future.value(true));
    ...
  });
}

class UrlLauncherPlatformMock extends Mock
    with MockPlatformInterfaceMixin
    implements UrlLauncherPlatform {}</code></pre><p>Unfortunately, <code>any</code> throws a compilation error:</p><blockquote>The argument type &apos;Null&apos; can&apos;t be assigned to the parameter type &apos;String&apos;.</blockquote><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/12/image.png" class="kg-image" alt loading="lazy" width="2000" height="406" srcset="https://www.wafrat.com/content/images/size/w600/2025/12/image.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/12/image.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/12/image.png 1600w, https://www.wafrat.com/content/images/2025/12/image.png 2268w" sizes="(min-width: 720px) 720px"></figure><p>This is because Mockito requires the method to accept nullable parameters for <code>any</code> to work. The problem is, the only way to generate methods that accept nullable parameters is to use Mockito&apos;s GenerateNiceMocks automated mock generation feature (<a href="https://pub.dev/packages/mockito#lets-create-mocks">https://pub.dev/packages/mockito#lets-create-mocks</a>, <a href="https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md">https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md</a>).</p><p>The problem is, if I use GenerateNiceMocks, then the generated mock will indeed let me use <code>any</code> to capture parameters, but it won&apos;t implement the right mixin, and <code>verify</code> will fail.</p><p>The solution is to first generate a mock with nullable parameters using GenerateNiceMocks, then subclass that with the right mixin.</p><pre><code class="language-dart">import &apos;package:flashcards/search_list_tile.dart&apos;;
import &apos;package:flutter/material.dart&apos;;
import &apos;package:flutter_test/flutter_test.dart&apos;;
import &apos;package:language_picker/languages.dart&apos;;
import &apos;package:mockito/annotations.dart&apos;;
import &apos;package:mockito/mockito.dart&apos;;
import &apos;package:plugin_platform_interface/plugin_platform_interface.dart&apos;;
import &apos;package:url_launcher_platform_interface/url_launcher_platform_interface.dart&apos;;

@GenerateNiceMocks([MockSpec&lt;UrlLauncherPlatform&gt;()])
import &apos;search_test.mocks.dart&apos;;

main() {
  testWidgets(&apos;Search menu icon&apos;, (WidgetTester tester) async {
    // Set up UrlLauncher mocks.
    final urlLauncherPlatformMock = MockUrlLauncherPlatformWithMixin();
    UrlLauncherPlatform.instance = urlLauncherPlatformMock;
    when(urlLauncherPlatformMock.canLaunch(any))
        .thenAnswer((_) async =&gt; Future.value(true));

    // Render.
    await tester.pumpWidget(MaterialApp(
        home: Scaffold(
            body: Center(
      child: SearchListTile.getSearchMenuIcon(
          Languages.korean, Languages.english, &apos;&#xCF67;&#xAD6C;&#xBA4D;&apos;),
    ))));
    await tester.pump();

    ...

    // Verify.
    verify(urlLauncherPlatformMock.launchUrl(
        &apos;https://dict.naver.com/search.nhn?query=%EC%BD%A7%EA%B5%AC%EB%A9%8D&apos;,
        any));
  });
}

class MockUrlLauncherPlatformWithMixin extends MockUrlLauncherPlatform
    with MockPlatformInterfaceMixin {}
</code></pre>]]></content:encoded></item><item><title><![CDATA[How to fix Null issue when using Mockito's `any` or `captureAny`]]></title><description><![CDATA[<p>This morning I was trying to verify an invocation with Mockito in Dart, but somehow it threw an error about Null.</p><blockquote>The argument type &apos;Null&apos; can&apos;t be assigned to the parameter type &apos;Todo&apos;. dart(argument_type_not_assignable)</blockquote><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/11/image.png" class="kg-image" alt loading="lazy" width="1686" height="618" srcset="https://www.wafrat.com/content/images/size/w600/2025/11/image.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/11/image.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/11/image.png 1600w, https://www.wafrat.com/content/images/2025/11/image.png 1686w" sizes="(min-width: 720px) 720px"></figure><p>A few searches proposed unsatisfactory workarounds,</p>]]></description><link>https://www.wafrat.com/how-to-fix-null-issue-when-using-mockitos-captureany/</link><guid isPermaLink="false">69153ea7021ca2165983d49d</guid><category><![CDATA[flutter]]></category><category><![CDATA[mockito]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Thu, 13 Nov 2025 02:24:53 GMT</pubDate><content:encoded><![CDATA[<p>This morning I was trying to verify an invocation with Mockito in Dart, but somehow it threw an error about Null.</p><blockquote>The argument type &apos;Null&apos; can&apos;t be assigned to the parameter type &apos;Todo&apos;. dart(argument_type_not_assignable)</blockquote><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/11/image.png" class="kg-image" alt loading="lazy" width="1686" height="618" srcset="https://www.wafrat.com/content/images/size/w600/2025/11/image.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/11/image.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/11/image.png 1600w, https://www.wafrat.com/content/images/2025/11/image.png 1686w" sizes="(min-width: 720px) 720px"></figure><p>A few searches proposed unsatisfactory workarounds, basically saying we should change our API to accept a nullable parameter:</p><ul><li><a href="https://stackoverflow.com/questions/66582801/after-migrating-flutter-code-to-null-safety-mock-objects-not-accepting-any">https://stackoverflow.com/questions/66582801/after-migrating-flutter-code-to-null-safety-mock-objects-not-accepting-any</a></li><li><a href="https://stackoverflow.com/questions/71230978/the-argument-type-null-cant-be-assigned-to-the-parameter-type-int-flutter">https://stackoverflow.com/questions/71230978/the-argument-type-null-cant-be-assigned-to-the-parameter-type-int-flutter</a></li></ul><p>The official Mockito doc about null safety isn&apos;t particularly useful either: <a href="https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#argument-matchers">https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#argument-matchers</a></p><p>Eventually, I found this:</p><ul><li><a href="https://github.com/dart-lang/mockito/issues/630#issuecomment-1631680087">https://github.com/dart-lang/mockito/issues/630#issuecomment-1631680087</a></li><li><a href="https://stackoverflow.com/a/74223774">https://stackoverflow.com/a/74223774</a></li></ul><p>What they mean is that you may implement a Mock that supports nullable types, without changing your actual API. What&apos;s more, <code>@GenerateNiceMocks</code> already does that for us.</p><p>So in my test, instead of defining my <code>mockTodoProvider</code> as a <code>TodoProvider</code>, I defined it as a <code>MockTodoProvider</code>, and now my test compiles!</p><p>Before:</p><pre><code class="language-dart">main() {
  late TodoProvider todoProvider;

  setUp(() async {
    todoProvider = MockTodoProvider();
  });
  
  testWidgets(...);
}</code></pre><p>After:</p><pre><code class="language-dart">main() {
  late MockTodoProvider todoProvider;

  setUp(() async {
    todoProvider = MockTodoProvider();
  });
  
  testWidgets(...);
}</code></pre><p>Now my test compiles and runs fine:</p><pre><code class="language-dart">testWidgets(&apos;delete todo&apos;, (WidgetTester tester) async {
  await tester.pumpWidget(MaterialApp(...));
  ...
  // Check the box.
  await tester.tap(find.byType(Checkbox));
  // Verify that update() has been called.
  final verificationResult = verify(p.update(captureAny));
  // Check that the passed Todo has been marked as done.
  final capturedTodo = verificationResult.captured.first;
  expect(capturedTodo.done, isTrue);
});</code></pre><p>Related docs:</p><ul><li><a href="https://pub.dev/documentation/mockito/latest/mockito/captureAny.html">https://pub.dev/documentation/mockito/latest/mockito/captureAny.html</a></li><li><a href="https://pub.dev/documentation/mockito/latest/mockito/VerificationResult/captured.html">https://pub.dev/documentation/mockito/latest/mockito/VerificationResult/captured.html</a></li></ul>]]></content:encoded></item><item><title><![CDATA[How to process a paste command but still let TextField handle it?]]></title><description><![CDATA[<p>It took me hours to figure out. I was not able to find sample code, and ChatGPT and Gemini kept hallucinating answers...</p><h1 id="basic-implementation-textfield-stops-responding-to-paste-events">Basic implementation. TextField stops responding to paste events</h1><p>Here&apos;s the usual way of handling a shortkey:</p><pre><code>Shortcuts(
  shortcuts: const &lt;ShortcutActivator, Intent&gt;{
    SingleActivator(
      LogicalKeyboardKey.keyV,</code></pre>]]></description><link>https://www.wafrat.com/how-to-process-paste-event-but-still-let-textfield/</link><guid isPermaLink="false">68cd412f021ca2165983d42a</guid><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Sat, 20 Sep 2025 00:15:11 GMT</pubDate><content:encoded><![CDATA[<p>It took me hours to figure out. I was not able to find sample code, and ChatGPT and Gemini kept hallucinating answers...</p><h1 id="basic-implementation-textfield-stops-responding-to-paste-events">Basic implementation. TextField stops responding to paste events</h1><p>Here&apos;s the usual way of handling a shortkey:</p><pre><code>Shortcuts(
  shortcuts: const &lt;ShortcutActivator, Intent&gt;{
    SingleActivator(
      LogicalKeyboardKey.keyV,
      // Command key on Mac.
      meta: true,
    ): PasteIntent(),
  },
  child: Actions(
      actions: {
        PasteIntent:
            CallbackAction&lt;PasteIntent&gt;(onInvoke: (intent) {
          // Do something
        })
      },
      child: TextField(...)
      ))


// Later define our custom Intent
class PasteIntent {}</code></pre><p>And it will indeed execute your code when you press Command+V. However, this will prevent the TextField from ever processing the command.</p><h1 id="attempting-to-pass-through-the-event-to-textfield">Attempting to pass through the event to TextField</h1><h2 id="donothingandstoppropagationintent">DoNothingAndStopPropagationIntent</h2><p>I found this &#xA0;<a href="https://api.flutter.dev/flutter/widgets/DoNothingAndStopPropagationIntent-class.html">DoNothingAndStopPropagationIntent</a> that supposedly &quot;disable[s] a keyboard shortcut defined by a widget higher in the widget hierarchy&quot;, so I thought I could create a Shortcuts, handle the event, then add another Shortcuts using that StopPropagationIntent that wraps the TextField. It did not work.</p><h2 id="reusing-pastetextintent-actionsmaybefind">Reusing PasteTextIntent, Actions.maybeFind</h2><p>Then I tried to reuse what I assumed was the original PasteIntent used by Flutter widgets themselves. Instead of mapping Command+V to my custom PasteIntent, I used <a href="https://api.flutter.dev/flutter/widgets/PasteTextIntent-class.html">PasteTextIntent</a>. Then in my callback, I tried to find other actions in the Context based on <a href="https://docs.flutter.dev/ui/interactivity/actions-and-shortcuts#invoking-actions">https://docs.flutter.dev/ui/interactivity/actions-and-shortcuts#invoking-actions</a>. Something like this:</p><pre><code class="language-dart">Action&lt;PasteTextIntent&gt;? paste = Actions.maybeFind&lt;PasteTextIntent&gt;(
  context,
);
paste?.invoke(intent);</code></pre><p>But it did not find TextField&apos;s Action for PasteTextIntent.</p><h2 id="consumeskey-false">consumesKey: false?</h2><p>Another thing I tried was to create a real Action instead of a CallbackAction, and implement Action&apos;s consumesKey.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://api.flutter.dev/flutter/widgets/Action/consumesKey.html"><div class="kg-bookmark-content"><div class="kg-bookmark-title">consumesKey method - Action class - widgets library - Dart API</div><div class="kg-bookmark-description">API docs for the consumesKey method from the Action class, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://api.flutter.dev/flutter/widgets/Action/static-assets/favicon.png?v1" alt><span class="kg-bookmark-author">Dart API</span></div></div></a></figure><p>By returning false, I hoped that Shortcut would think my Action didn&apos;t consume the event, and propagate it down to the TextField. This did not work either.</p><h1 id="the-solution-actionoverridable">The solution: Action.overridable</h1><p>Eventually, I stumbled upon Action.overridable and that&apos;s how I fixed my issue. Action.overridable makes someone allow someone else to override their Action. The beauty of it is that if you override that Action, you also get a reference to the default action.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://api.flutter.dev/flutter/widgets/Action/Action.overridable.html"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Action.overridable constructor - Action - widgets library - Dart API</div><div class="kg-bookmark-description">API docs for the Action.overridable constructor from Class Action from the widgets library, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://api.flutter.dev/flutter/widgets/Action/static-assets/favicon.png?v1" alt><span class="kg-bookmark-author">Dart API</span></div></div></a></figure><p>The issue is, it only works if the creator of the original Action allows their Action to be overridable. Does TextField allow it?</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://api.flutter.dev/flutter/widgets/DefaultTextEditingShortcuts-class.html"><div class="kg-bookmark-content"><div class="kg-bookmark-title">DefaultTextEditingShortcuts class - widgets library - Dart API</div><div class="kg-bookmark-description">API docs for the DefaultTextEditingShortcuts class from the widgets library, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://api.flutter.dev/flutter/widgets/static-assets/favicon.png?v1" alt><span class="kg-bookmark-author">Dart API</span></div></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/flutter/flutter/blob/7ade3f344755eb86b20aae39610a5758b7e510d8/packages/flutter/lib/src/widgets/editable_text.dart#L714"><div class="kg-bookmark-content"><div class="kg-bookmark-title">flutter/packages/flutter/lib/src/widgets/editable_text.dart at 7ade3f344755eb86b20aae39610a5758b7e510d8 &#xB7; flutter/flutter</div><div class="kg-bookmark-description">Flutter makes it easy and fast to build beautiful apps for mobile and beyond - flutter/flutter</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">flutter</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1" alt></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/flutter/flutter/blob/7ade3f344755eb86b20aae39610a5758b7e510d8/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart#L297"><div class="kg-bookmark-content"><div class="kg-bookmark-title">flutter/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart at 7ade3f344755eb86b20aae39610a5758b7e510d8 &#xB7; flutter/flutter</div><div class="kg-bookmark-description">Flutter makes it easy and fast to build beautiful apps for mobile and beyond - flutter/flutter</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">flutter</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://repository-images.githubusercontent.com/31792824/fb7e5700-6ccc-11e9-83fe-f602e1e1a9f1" alt></div></a></figure><p>And the answer is YES! So here&apos;s my final solution. This will execute my code to handle Command+V but also let TextField handle it:</p><pre><code class="language-dart">Shortcuts(
  shortcuts: const &lt;ShortcutActivator, Intent&gt;{
    SingleActivator(
      LogicalKeyboardKey.keyV,
      // command on Mac.
      meta: true,
    ): PasteTextIntent(SelectionChangedCause.keyboard),
  },
  child: Actions(
      actions: &lt;Type, Action&lt;Intent&gt;&gt;{
        // Override TextField&apos;s handler to also support images.
        PasteTextIntent:
            PasteScreenshotAction(onFilePasted: (xfile) {
          filesPicked.add([xfile]);
        })
      },
      child: TextField(...)
  ))
...

class PasteScreenshotAction extends Action&lt;PasteTextIntent&gt; {
  PasteScreenshotAction({required this.onFilePasted});

  final void Function(XFile xfile) onFilePasted;

  @override
  Object? invoke(covariant PasteTextIntent intent) async {
    // Let TextField process the paste.
    callingAction?.invoke(intent);

    // And also handle images from the clipboard.
    ..
  }
}
</code></pre>]]></content:encoded></item><item><title><![CDATA[Mocking time in UI tests in Flutter]]></title><description><![CDATA[<p>When working with tests that test logic related to time, it is necessary to mock the time, especially if that time is being rendered in screenshot tests.</p><p>My code from the stone age would take an optional <code>DateTime? now</code> parameter to mock it. Then in the method, I would run</p>]]></description><link>https://www.wafrat.com/mocking-time-in-ui-tests-in-flutter/</link><guid isPermaLink="false">68c8b192021ca2165983d3ca</guid><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Tue, 16 Sep 2025 01:24:30 GMT</pubDate><content:encoded><![CDATA[<p>When working with tests that test logic related to time, it is necessary to mock the time, especially if that time is being rendered in screenshot tests.</p><p>My code from the stone age would take an optional <code>DateTime? now</code> parameter to mock it. Then in the method, I would run <code>now ?? DateTime.now()</code> to determine the current time. But the cleaner way is to use <code>Clock</code>.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/clock"><div class="kg-bookmark-content"><div class="kg-bookmark-title">clock | Dart package</div><div class="kg-bookmark-description">A fakeable wrapper for dart:core clock APIs.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-ed1s4f5b/img/pub-dev-icon-cover-image.png" alt></div></a></figure><p>So that&apos;s what I did. Before:</p><pre><code class="language-dart">testWidgets(&apos;share text&apos;, (WidgetTester tester) async {
  initializeTimeZones();
  await initializeDateFormatting(AppLocale);
  await loadFont();

  ...
  await tester.pumpWidget(
    MyApp(now: todayAtNoon),
  );
  await tester.pumpAndSettle();

  await expectLater(
    find.byType(MaterialApp),
    matchesGoldenFile(
      &apos;goldens/app/iosShareText1.png&apos;,
    ),
  );

  await tester.tap(find.byIcon(Icons.send).first);
  await tester.pumpAndSettle();
  // TODO: fix clock issues to show the message having been sent.
  await expectLater(
    find.byType(MaterialApp),
    matchesGoldenFile(
      &apos;goldens/app/iosShareText2Sent.png&apos;,
    ),
  );
});

testWidgets(&apos;share image&apos;, (WidgetTester tester) async {
  ...
});</code></pre><p>After:</p><pre><code class="language-dart">withClock(Clock.fixed(todayAtNoon.add(Duration(hours: 3))), () async {
  testWidgets(&apos;share text&apos;, (WidgetTester tester) async {
    initializeTimeZones();
    await initializeDateFormatting(AppLocale);
    await loadFont();

    ...
    await tester.pumpWidget(
      MyApp(),
    );
    await tester.pumpAndSettle();

    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile(
        &apos;goldens/app/iosShareText1.png&apos;,
      ),
    );

    await tester.tap(find.byIcon(Icons.send).first);
    await tester.pumpAndSettle();
    // TODO: fix clock issues to show the message having been sent.
    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile(
        &apos;goldens/app/iosShareText2Sent.png&apos;,
      ),
    );
  });

  testWidgets(&apos;share image&apos;, (WidgetTester tester) async {
    ...
  });
});</code></pre><p>I wrapped all of my tests in a withClock instead of using withClock in each testWidgets to not have to write the same withClock boilerplate. However, in my screenshot, the time was still not the mocked time. It was the actual current time!</p><p>So I added this code inside the withClock and inside the testWidgets to check the value of clock:</p><pre><code class="language-dart">print(clock.now());</code></pre><p>And indeed, as soon as in goes inside testWidgets, clock gets reset back.</p><p>So I had to invert how I set up time and add withClock inside each testWidgets:</p><pre><code class="language-dart">testWidgets(&apos;share text&apos;, (WidgetTester tester) async {
  withClock(Clock.fixed(todayAtNoon.add(Duration(hours: 3))), () async {
    initializeTimeZones();
    await initializeDateFormatting(AppLocale);
    await loadFont();

    ...
    await tester.pumpWidget(
      MyApp(),
    );
    await tester.pumpAndSettle();

    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile(
        &apos;goldens/app/iosShareText1.png&apos;,
      ),
    );

    await tester.tap(find.byIcon(Icons.send).first);
    await tester.pumpAndSettle();
    // TODO: fix clock issues to show the message having been sent.
    await expectLater(
      find.byType(MaterialApp),
      matchesGoldenFile(
        &apos;goldens/app/iosShareText2Sent.png&apos;,
      ),
    );
  });

  testWidgets(&apos;share image&apos;, (WidgetTester tester) async {
    withClock(Clock.fixed(todayAtNoon.add(Duration(hours: 3))), () async {
      ...
    });
  });
});</code></pre><p>Now the screenshot shows up correctly with the mocked time, but I get this error:</p><pre><code>&#x2550;&#x2550;&#x2561; EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK &#x255E;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;
The following StateError was thrown running a test (but after the test had completed):
Bad state: No element

When the exception was thrown, this was the stack:
#0      Iterable.first (dart:core/iterable.dart:663:7)
#1      _FirstFinderMixin.filter (package:flutter_test/src/finders.dart:1332:28)
#3      Iterable.isEmpty (dart:core/iterable.dart:560:33)
#4      WidgetController._getElementPoint (package:flutter_test/src/controller.dart:2008:18)
#5      WidgetController.getCenter (package:flutter_test/src/controller.dart:1861:12)
#6      WidgetController.tap (package:flutter_test/src/controller.dart:1041:7)
#7      main.&lt;anonymous closure&gt;.&lt;anonymous closure&gt;.&lt;anonymous closure&gt; (file:///[project_path]/test/app_test.dart:216:22)
&lt;asynchronous suspension&gt;
(elided one frame from dart:async-patch)
&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;&#x2550;</code></pre><p>How can it not find the Send icon button? I can see it fine in the screenshot.</p><p>Here&apos;s what Google AI Overview has to say about clock:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-20.png" class="kg-image" alt loading="lazy" width="1310" height="2444" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-20.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-20.png 1000w, https://www.wafrat.com/content/images/2025/09/image-20.png 1310w" sizes="(min-width: 720px) 720px"></figure><p>Really? clock is automatically mocked and set to Jan 1, 2015? So I try it:</p><pre><code>void main() {
  testWidgets(&apos;signs in&apos;, (WidgetTester tester) async {
    print(clock.now());
    ...</code></pre><p>Nope, it shows me the current time.</p><p>After some more research, I find this post: <a href="https://docs.flutter.dev/release/breaking-changes/test-widgets-flutter-binding-clock">https://docs.flutter.dev/release/breaking-changes/test-widgets-flutter-binding-clock</a>. It mentions <code>tester.binding.clock</code>. Let&apos;s try it:</p><pre><code class="language-dart">void main() {
  testWidgets(&apos;signs in&apos;, (WidgetTester tester) async {
    print(clock.now());
    print(tester.binding.clock.now());
</code></pre><p>And it outputs:</p><pre><code>00:01 +0: signs in
2025-09-16 09:55:26.754504
2015-01-01 00:00:00.000Z</code></pre><p>WOW!</p><p>What if I start a zone with withClock that uses the same Clock? Then the test and the logic&apos;s Clock would match, and pump would advance the same time...</p><pre><code class="language-dart">void main() {
  testWidgets(&apos;signs in&apos;, (WidgetTester tester) async {
    withClock(tester.binding.clock, () async {
      print(clock.now());
      print(tester.binding.clock.now());

      initializeTimeZones();
      await initializeDateFormatting(AppLocale);
      await loadFont();
      ...
      await tester.pumpWidget(
        MyApp(),
      );

      expect(find.text(&apos;Sign in&apos;), findsOneWidget);
</code></pre><p>Nope, it still cannot find the button.</p><p>So I searched some more, and stumbled upon this blog post: <a href="https://retired.re-ynd.com/friends-roster-testing-in-flutter/">https://retired.re-ynd.com/friends-roster-testing-in-flutter/</a>.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-21.png" class="kg-image" alt loading="lazy" width="1426" height="1846" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-21.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-21.png 1000w, https://www.wafrat.com/content/images/2025/09/image-21.png 1426w" sizes="(min-width: 720px) 720px"></figure><p>He did the same: testWidgets inside of which he calls withClock, then renders his UI, and finds the element correctly. How come?</p><p>The only difference I can find is he awaits <code>withClock</code>. Let&apos;s try that.</p><pre><code class="language-dart">void main() {
  testWidgets(&apos;signs in&apos;, (WidgetTester tester) async {
    await withClock(tester.binding.clock, () async {
      initializeTimeZones();
      await initializeDateFormatting(AppLocale);
      await loadFont();
      ...
      await tester.pumpWidget(
        MyApp(),
      );

      expect(find.text(&apos;Connexion&apos;), findsOneWidget);
      expect(find.text(&apos;hello&apos;), findsNothing);

      await expectLater(
        find.byType(MaterialApp),
        matchesGoldenFile(
          &apos;goldens/app/signedOut.png&apos;,
        ),
      );
      ...</code></pre><p>Mmh... It actually works!! Haha. Somehow I expected withClock to be synchronous, but it actually returns the result of the callback, which is a <code>Future</code> since I am using <code>async</code>.</p>]]></content:encoded></item><item><title><![CDATA[Using Firebase Storage on Flutter Web]]></title><description><![CDATA[<p>My app was working fine on Android, iOS and MacOS, but it wouldn&apos;t display images stored in Firebase Storage. It would fail with the dreaded CORS error.</p><p>I need to somehow whitelist localhost:5000 for Firebase Storage. It is well documented at https://firebase.google.com/docs/storage/</p>]]></description><link>https://www.wafrat.com/using-firebase-storage-on-flutter-web/</link><guid isPermaLink="false">68be4691021ca2165983d394</guid><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Mon, 08 Sep 2025 03:30:53 GMT</pubDate><content:encoded><![CDATA[<p>My app was working fine on Android, iOS and MacOS, but it wouldn&apos;t display images stored in Firebase Storage. It would fail with the dreaded CORS error.</p><p>I need to somehow whitelist localhost:5000 for Firebase Storage. It is well documented at https://firebase.google.com/docs/storage/web/download-files#cors_configuration. It&apos;s just extra work.</p><p>First I needed to install gsutil ( <a href="https://cloud.google.com/storage/docs/gsutil_install">https://cloud.google.com/storage/docs/gsutil_install</a>) and sign in with <code>gcloud init</code>.</p><p>Next, I create my cors.json file.</p><pre><code class="language-json">[
  {
    &quot;origin&quot;: [&quot;localhost:5000&quot;, &quot;[production domain]&quot;],
    &quot;method&quot;: [&quot;GET&quot;],
    &quot;maxAgeSeconds&quot;: 3600
  }
]</code></pre><p>Finally I apply the settings:</p><pre><code>% gsutil cors set cors.json gs://[project-id]
Setting CORS on gs://[project-id]/...
NotFoundException: 404 The specified bucket does not exist.

% gsutil cors set cors.json gs://[project-id].appspot.com
Setting CORS on gs://[project-id].appspot.com/...</code></pre><p>But I still got the same error.</p><p>So instead I tried to temporarily whitelist everything: <code>&quot;origin&quot;: &quot;*&quot;</code>. And it worked: my app displayed the image from Firebase Storage! So it looks like it is not enough to specify the domain, I need to specify whether it&apos;s http or https:</p><pre><code>[
  {
    &quot;origin&quot;: [&quot;http://localhost:5000&quot;, &quot;https://[production domain]&quot;],
    &quot;method&quot;: [&quot;GET&quot;],
    &quot;maxAgeSeconds&quot;: 3600
  }
]</code></pre><p>And indeed, it worked with this more restrictive setting.</p>]]></content:encoded></item><item><title><![CDATA[Google Sign-in for Flutter Web]]></title><description><![CDATA[<p>My Flutter app was running fine for Web until I migrated google_sign_in from 6 to 7. Now signing-in fails with this error: <code>Sign in failed: UnimplementedError: authenticate is not supported on the web. Instead, use renderButton to create a sign-in widget</code>. Let&apos;s fix it.</p><p>Up until</p>]]></description><link>https://www.wafrat.com/google-sign-in-for-flutter-web/</link><guid isPermaLink="false">68be3cb9021ca2165983d33b</guid><category><![CDATA[flutter web]]></category><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Mon, 08 Sep 2025 02:54:08 GMT</pubDate><content:encoded><![CDATA[<p>My Flutter app was running fine for Web until I migrated google_sign_in from 6 to 7. Now signing-in fails with this error: <code>Sign in failed: UnimplementedError: authenticate is not supported on the web. Instead, use renderButton to create a sign-in widget</code>. Let&apos;s fix it.</p><p>Up until google_sign_in 6, it seemed like I could use the same code to sign in on iOS and Web. But from version 7, authentication on Web is different.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/google_sign_in"><div class="kg-bookmark-content"><div class="kg-bookmark-title">google_sign_in | Flutter package</div><div class="kg-bookmark-description">Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/static/hash-e4t06sub/img/flutter-logo-32x32.png" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/google_sign_in_web#authentication"><div class="kg-bookmark-content"><div class="kg-bookmark-title">google_sign_in_web | Flutter package</div><div class="kg-bookmark-description">Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/static/hash-e4t06sub/img/flutter-logo-32x32.png" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><blockquote>On the web, instead of providing custom UI that calls authenticate, you should display the Widget returned by <code>renderButton</code> (from web_only.dart), and listen to <code>authenticationEvents</code> to know when the user has signed in.</blockquote><h1 id="adding-the-button">Adding the button</h1><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/documentation/google_sign_in_web/latest/web_only/renderButton.html"><div class="kg-bookmark-content"><div class="kg-bookmark-title">renderButton function - web_only library - Dart API</div><div class="kg-bookmark-description">API docs for the renderButton function from the web_only library, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart API</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/dart-logo.svg" alt></div></a></figure><p>I changed my code to render that special button on Web:</p><p>Before:</p><pre><code class="language-dart">ElevatedButton(
    child: Text(&apos;Sign in&apos;),
    onPressed: () {
      _handleSignIn(widget.auth, widget.googleSignIn);
    })</code></pre><p>After:</p><pre><code class="language-dart">!kIsWeb
    ? ElevatedButton(
        child: Text(&apos;Sign in&apos;),
        onPressed: () {
          _handleSignIn(widget.auth, widget.googleSignIn);
        })
    : renderButton()
</code></pre><h1 id="adding-the-dependency">Adding the dependency</h1><p>However, VSC couldn&apos;t find the renderButton. <a href="https://pub.dev/packages/google_sign_in_web">https://pub.dev/packages/google_sign_in_web</a> has the explanation why:</p><blockquote>This package is endorsed, which means you can simply use google_sign_in normally. This package will be automatically included in your app when you do, so you do not need to add it to your pubspec.yaml. However, if you import this package to use any of its APIs directly, you should add it to your pubspec.yaml as usual. <strong>For example, you need to import this package directly if you plan to use the web-only Widget renderButton() method.</strong></blockquote><p>So I do need to manually import this package.</p><h1 id="init">init()</h1><p>After I add the renderButton, I am getting the following error:</p><blockquote>Bad state: GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() must be called before any other method in this plugin.</blockquote><p>Looking back at <a href="https://pub.dev/packages/google_sign_in#usage">https://pub.dev/packages/google_sign_in#usage</a>, I have all the information I need to initialize it in code:</p><pre><code class="language-dart">unawaited(_googleSignIn.initialize(
        clientId:
            &apos;....apps.googleusercontent.com&apos;));</code></pre><p>Back in version 6, I had two choices to initialize GoogleSignIn. I could either pass the client id in code, or add a &lt;meta&gt; tag. I guess the meta tag way does not work anymore.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-18.png" class="kg-image" alt loading="lazy" width="478" height="196"></figure><h1 id="running-with-a-fixed-port">Running with a fixed port</h1><p>If I attempt to run in Chrome using VSC&apos;s built-in Play button, the web app runs on a random port every time. Then Google Sign-In will throw an <em>Access blocked: Authorization Error, Error 400: origin_mismatch</em>.</p><p>So I need to run the exact port that I whitelisted on the Cloud Console:</p><pre><code>flutter run -d chrome --web-port 5000</code></pre><p>After setting it all up, Google Sign-In works on Web again! But wait a second...</p><h1 id="restoring-compilation-for-ios">Restoring compilation for iOS</h1><p>Adding a dependency to google_sign_in_web actually broke compilation on other platforms like iOS. When I try to run the app on iOS, I get those errors:</p><pre><code> ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1067:40: Error: &apos;JSObject&apos; isn&apos;t a type.
    extension type RTCRtpTransceiverInit._(JSObject _) implements JSObject {
                                           ^^^^^^^^
    ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1070:5: Error: &apos;JSArray&apos; isn&apos;t a type.
        JSArray&lt;MediaStream&gt; streams,
        ^^^^^^^
    ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1071:5: Error: &apos;JSArray&apos; isn&apos;t a type.
        JSArray&lt;RTCRtpEncodingParameters&gt; sendEncodings,
        ^^^^^^^
...

    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:79:51: Error: The getter &apos;toJS&apos; isn&apos;t defined for the
    type &apos;void Function(TokenRevocationResponse)&apos;.
    Try correcting the name to the name of an existing getter, or defining a getter or field named &apos;toJS&apos;.
        return _revokeWithDone(accessToken.toJS, done.toJS);
                                                      ^^^^
    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:83:25: Error: &apos;JSString&apos; isn&apos;t a type.
      external void _revoke(JSString accessToken);
                            ^^^^^^^^
    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:85:33: Error: &apos;JSString&apos; isn&apos;t a type.
      external void _revokeWithDone(JSString accessToken, JSFunction done);
                                    ^^^^^^^^</code></pre><p>Luckily Gemini has a good way to isolate the code.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-19.png" class="kg-image" alt loading="lazy" width="1216" height="2074" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-19.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-19.png 1000w, https://www.wafrat.com/content/images/2025/09/image-19.png 1216w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-code-card"><pre><code class="language-dart">import &apos;package:flutter/material.dart&apos;;
import &apos;package:google_sign_in_web/web_only.dart&apos;;

class SignInButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return renderButton();
  }
}</code></pre><figcaption>sign_in_button_web.dart</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-dart">import &apos;package:flutter/material.dart&apos;;
import &apos;package:google_sign_in/google_sign_in.dart&apos;;
import &apos;package:provider/provider.dart&apos;;

class SignInButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final googleSignIn = context.read&lt;GoogleSignIn&gt;();

    return ElevatedButton(
        child: Text(&apos;Sign in&apos;),
        onPressed: () {
          googleSignIn.authenticate();
        });
  }
}</code></pre><figcaption>sign_in_button_not_web.dart</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-dart">// Default export for mobile/desktop
export &apos;sign_in_button_not_web.dart&apos;
    // Conditional export for web
    if (dart.library.js_interop) &apos;sign_in_button_web.dart&apos;;</code></pre><figcaption>sign_in_button.dart</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-dart">import &apos;package:elysium/sign_in_button.dart&apos;;

Provider(
    create: (context) =&gt; widget.googleSignIn,
    child: SignInButton())</code></pre><figcaption>Usage</figcaption></figure><p>And indeed, it compiled fine:</p><pre><code>Running Xcode build...                                                  
Xcode build done.                                           40.6s
&#x2713; Built build/ios/iphoneos/Runner.app (50.1MB)</code></pre><h2 id="conditional-initialize">Conditional initialize</h2><p>When I click on Sign in, I now get an error about not supporting the right URL. That&apos;s because I initialized GoogleSignIn with the key used for Web, whereas I configured my iOS app to use other keys.</p><p>Before:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">unawaited(_googleSignIn.initialize(
        clientId:
            &apos;[...].apps.googleusercontent.com&apos;));</code></pre><figcaption>Uses the Web key by default</figcaption></figure><p>After:</p><figure class="kg-card kg-code-card"><pre><code class="language-dart">unawaited(_googleSignIn.initialize(
    clientId: kIsWeb
        ? &apos;[...].apps.googleusercontent.com&apos;
        : null));
</code></pre><figcaption>Only uses the Web key on Web</figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Flutter for MacOS]]></title><description><![CDATA[<p>My first case study for building apps for MacOS was to use Apple&apos;s built-in capability of running iOS apps natively on Macs with Apple Silicon (see <a href="https://www.wafrat.com/flutter-ios-app-for-macos/">https://www.wafrat.com/flutter-ios-app-for-macos/</a>). This time, since I want to use features that may not be available on iOS such as</p>]]></description><link>https://www.wafrat.com/flutter-for-macos/</link><guid isPermaLink="false">68be1f90021ca2165983d28c</guid><category><![CDATA[flutter]]></category><category><![CDATA[macos]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Mon, 08 Sep 2025 01:15:39 GMT</pubDate><content:encoded><![CDATA[<p>My first case study for building apps for MacOS was to use Apple&apos;s built-in capability of running iOS apps natively on Macs with Apple Silicon (see <a href="https://www.wafrat.com/flutter-ios-app-for-macos/">https://www.wafrat.com/flutter-ios-app-for-macos/</a>). This time, since I want to use features that may not be available on iOS such as a proper File Picker, I want to make compile my Flutter app specifically for MacOS.</p><h1 id="google-sign-in">Google Sign-In</h1><p>Starting from <a href="https://pub.dev/packages/google_sign_in#platform-integration">https://pub.dev/packages/google_sign_in#platform-integration</a>, I am told to refer to the integration steps for iOS and MacOS: <a href="https://pub.dev/packages/google_sign_in_ios#macos-integration">https://pub.dev/packages/google_sign_in_ios#macos-integration</a>.</p><p>So I set up <code>[my_project]/macos/Runner/Info.plist</code> the same was as I set up <code>[my_project]/ios/Runner/Info.plist</code>. I also add this additional key-value as instructed:</p><pre><code>&lt;key&gt;keychain-access-groups&lt;/key&gt;
&lt;array&gt;
    &lt;string&gt;$(AppIdentifierPrefix)com.google.GIDSignIn&lt;/string&gt;
&lt;/array&gt;</code></pre><p>When I run the app and try to sign in, I can enter my credentials and accept what permissions I grant the app upon signing-in, but as soon as I click &quot;Next&quot;, the Google sign-in page disappears, but my app does not receive any Google credential. There is also no error in the console.</p><h2 id="printing-out-errors">Printing out errors</h2><p>In <a href="https://pub.dev/packages/google_sign_in#initialization-and-authentication">the documentation</a>, they show how listen for errors. However I could not find this API in google_sign_in 6. So I had to migrate to the 7, which is the latest major version.</p><pre><code class="language-dart">  googleSignIn.authenticationEvents.listen((e) {
    print(&apos;event&apos;);
    print(e);
  }).onError((e) {
    print(&apos;error&apos;);
    print(e);
  });
</code></pre><p>Now I can remove the configuration and add them back one by one to make sure they are truly necessary.</p><h2 id="gidclientid">GIDClientID</h2><p>This time, upon clicking the Sign-In button, the Google Sign-In page does not pop up. Instead I get this message.</p><pre><code>flutter: Sign in failed: PlatformException(google_sign_in, No active configuration. Make sure GIDClientID is set in Info.plist., NSInvalidArgumentException, null)
</code></pre><p>Good, let&apos;s add that back to <code>[my_project]/macos/Runner/Info.plist</code>.</p><h2 id="cfbundleurlschemes">CFBundleURLSchemes</h2><pre><code>flutter: Sign in failed: PlatformException(google_sign_in, Your app is missing support for the following URL schemes: com.googleusercontent.apps.[redacted], NSInvalidArgumentException, null)</code></pre><p>After adding this step, the Google Sign-In page does pop up correctly and lets me enter my credentials. Upon clicking &quot;Continue&quot; to finish the sign-in process, my app throws this error:</p><pre><code>flutter: Sign in failed: PlatformException(org.openid.appauth.general: -5, Connection error making token request to &apos;https://oauth2.googleapis.com/token&apos;: The operation couldn&#x2019;t be completed. Operation not permitted., {NSLocalizedDescription: Connection error making token request to &apos;https://oauth2.googleapis.com/token&apos;: The operation couldn&#x2019;t be completed. Operation not permitted., NSUnderlyingError: {code: 1, domain: NSPOSIXErrorDomain, localizedDescription: The operation couldn&#x2019;t be completed. Operation not permitted, userInfo: {_NSURLErrorFailingURLSessionTaskErrorKey: LocalDataTask &lt;FBF44A34-B484-4D53-8E25-F065580CA914&gt;.&lt;1&gt;, _kCFStreamErrorDomainKey: 1, _kCFStreamErrorCodeKey: 1, _NSURLErrorRelatedURLSessionTaskErrorKey: [LocalDataTask &lt;FBF44A34-B484-4D53-8E25-F065580CA914&gt;.&lt;1&gt;], _NSURLErrorNWPathKey: [Unsupported type: NWConcrete_nw_path]}}}, null)</code></pre><h2 id="outgoing-network-connections-not-documented">Outgoing Network Connections (not documented!)</h2><p>This is actually not documented in the google_sign_in documentation on pub.dev. When I asked Gemini about it, it revealed that I should enable outgoing network connections in XCode.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-12.png" class="kg-image" alt loading="lazy" width="1566" height="2084" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-12.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-12.png 1000w, https://www.wafrat.com/content/images/2025/09/image-12.png 1566w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.wafrat.com/content/images/2025/09/-----------2025-09-08-------9.40.52.png" class="kg-image" alt loading="lazy" width="1072" height="428" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/-----------2025-09-08-------9.40.52.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/-----------2025-09-08-------9.40.52.png 1000w, https://www.wafrat.com/content/images/2025/09/-----------2025-09-08-------9.40.52.png 1072w" sizes="(min-width: 720px) 720px"><figcaption>Gotta check that box</figcaption></figure><h2 id="incoming-connections">Incoming Connections?</h2><p>When I wrote this post, I noticed that Incoming Connections (Server) was already checked, and I was not sure why. So I disabled it, and now I know:</p><pre><code>&#x2713; Built build/macos/Build/Products/Debug/elysium_mobile_client.app
2025-09-08 09:42:29.794 [app_name] Running with merged UI and platform thread. Experimental.
flutter: Could not start Dart VM service HTTP server:
SocketException: Failed to create server socket (OS Error: Operation not permitted, errno = 1), address = 127.0.0.1, port = 0
#0      _NativeSocket.bind (dart:io-patch/socket_patch.dart:1220:7)
&lt;asynchronous suspension&gt;
#1      _RawServerSocket.bind.&lt;anonymous closure&gt; (dart:io-patch/socket_patch.dart:2166:12)
&lt;asynchronous suspension&gt;
#2      _ServerSocket.bind.&lt;anonymous closure&gt; (dart:io-patch/socket_patch.dart:2521:12)
&lt;asynchronous suspension&gt;
#3      _HttpServer.bind.&lt;anonymous closure&gt; (dart:_http/http_impl.dart:3506:24)
&lt;asynchronous suspension&gt;
#4      Server.startup.startServer (dart:vmservice_io/vmservice_server.dart:317:23)
&lt;asynchronous suspension&gt;
#5      Server.startup (dart:vmservice_io/vmservice_server.dart:339:11)
&lt;asynchronous suspension&gt;
#6      _toggleWebServer (dart:vmservice_io:229:5)
&lt;asynchronous suspension&gt;

2025-09-08 09:42:30.134 [app_name] The operation couldn&#x2019;t be completed. (OSStatus error 13.)
```</code></pre><p>Flutter needs to connect to it to provide debugging information.</p><h2 id="keychain-access-groups-entitlement">keychain-access-groups entitlement</h2><p>After restoring the Incoming Connections Capability, I am getting the next error:</p><pre><code>flutter: Sign in failed: GoogleSignInException(code GoogleSignInExceptionCode.providerConfigurationError, keychain error, {NSLocalizedDescription: keychain error})</code></pre><p>This one looks familiar. It is the one that <a href="https://pub.dev/packages/google_sign_in_ios#macos-integration">https://pub.dev/packages/google_sign_in_ios#macos-integration</a> mentioned:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-13.png" class="kg-image" alt loading="lazy" width="1632" height="622" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-13.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-13.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-13.png 1600w, https://www.wafrat.com/content/images/2025/09/image-13.png 1632w" sizes="(min-width: 720px) 720px"></figure><p>However, the error persisted even after adding this key/value to <code>Info.plist</code>. So I read about it a bit more on <a href="https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox">https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox</a>. It turns out entitlements are written in another file: <code>macos/Runner/DebugProfile.entitlements</code>.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-15.png" class="kg-image" alt loading="lazy" width="2000" height="953" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-15.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-15.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-15.png 1600w, https://www.wafrat.com/content/images/2025/09/image-15.png 2354w" sizes="(min-width: 720px) 720px"></figure><p>Gemini suggested to add the Capability using the XCode UI itself. That would have prevented my mistake.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-14.png" class="kg-image" alt loading="lazy" width="1494" height="2040" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-14.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-14.png 1000w, https://www.wafrat.com/content/images/2025/09/image-14.png 1494w" sizes="(min-width: 720px) 720px"></figure><h2 id="weird-xcode-compilation-setting">Weird XCode compilation setting</h2><p>After adding the Keychain Sharing Capability, my app wouldn&apos;t launch anymore. When I checked in XCode, I saw this new error:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-17.png" class="kg-image" alt loading="lazy" width="2000" height="359" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-17.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-17.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-17.png 1600w, https://www.wafrat.com/content/images/2025/09/image-17.png 2152w" sizes="(min-width: 720px) 720px"></figure><p>I had to Enable Development Signing for my app to run again.</p><h2 id="google-people-api-not-documented">Google People API (not documented!)</h2><p>At some point in the process, another error I got from google_sign_in was that I should enable the Google People API at <a href="https://console.cloud.google.com/apis/library/people.googleapis.com">https://console.cloud.google.com/apis/library/people.googleapis.com</a>. So I did, but what is puzzling is, I have never had this error on iOS.</p><h2 id="success">Success</h2><p>After setting all of this up, my Flutter app compile for MacOS and I can successfully sign into my Google Account!</p><h1 id="publishing-to-testflight">Publishing to TestFlight</h1><p>Reference docs:</p><ul><li><a href="https://docs.fastlane.tools/actions/default_platform/">https://docs.fastlane.tools/actions/default_platform/</a></li><li><a href="https://docs.flutter.dev/deployment/macos">https://docs.flutter.dev/deployment/macos</a></li></ul><pre><code>+ xcodebuild -exportArchive -exportOptionsPlist ...
2025-09-08 14:30:08.535 xcodebuild[7313:3471315] [MT] IDEDistribution: Command line name &quot;app-store&quot; is deprecated. Use &quot;app-store-connect&quot; instead.
error: exportArchive No signing certificate &quot;Mac Installer Distribution&quot; found

error: exportArchive No profiles for &apos;[package name]&apos; were found

** EXPORT FAILED **
[14:30:32]: Exit status: 70</code></pre><h2 id="validating">Validating</h2><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.apple.com/documentation/bundleresources/information-property-list/lsapplicationcategorytype"><div class="kg-bookmark-content"><div class="kg-bookmark-title">LSApplicationCategoryType | Apple Developer Documentation</div><div class="kg-bookmark-description">The category that best describes your app for the App Store.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.apple.com/apple-logo.svg" alt><span class="kg-bookmark-author">Apple Developer Documentation</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://docs.developer.apple.com/tutorials/developer-og.jpg" alt></div></a></figure><blockquote>The value of the CFBundleDocumentTypes key in the Info.plist must be an array of dictionaries, with each dictionary containing at least the CFBundleTypeName key. (ID: 76cfdd64-fce6-4823-88c3-5791ae6d9895)</blockquote><p>That&apos;s becaeuse I accidentally added an empty one by clicking around the XCode UI earlier. Removing this:</p><pre><code>	&lt;key&gt;CFBundleDocumentTypes&lt;/key&gt;
	&lt;array&gt;
		&lt;dict/&gt;
	&lt;/array&gt;
</code></pre><h2 id="publishing-to-testflight-1">Publishing to TestFlight</h2><p>After the few tweaks to Info.plist, XCode successfully validates. So I try to Distribute to TestFlight, and that steps successfully completes too. As soon as that step completes, I head over App Store Connect, and I do see that a build for macOS has appeared in my app&apos;s TestFlight tab!</p><p>At first the tab is gray, but after a few minutes, the UI renders and shows that its status is Processing.</p><h2 id="fastlane">Fastlane</h2><p>Now that I&apos;ve successfully distributed a macOS built to TestFlight, I am hoping that the Mac Installer Distribution error I encountered with Fastlane has been resolved. Let&apos;s try to publish another build again with Fastlane.</p><p>I increment my app version in pubspec.yaml, then try again.</p><p>And sure enough, it worked!</p><pre><code>[15:47:48]: Successfully exported and compressed dSYM file
[15:47:48]: Successfully exported the .app file:
[15:47:48]: ./[app name].app
[15:47:48]: Generated plist file with the following values:
[15:47:48]: &#x25B8; -----------------------------------------
[15:47:48]: &#x25B8; {
[15:47:48]: &#x25B8;   &quot;method&quot;: &quot;app-store&quot;
[15:47:48]: &#x25B8; }
[15:47:48]: &#x25B8; -----------------------------------------
...
[15:49:41]: Successfully exported and signed the pkg file:
[15:49:41]: ----------------------------------
[15:49:41]: --- Step: upload_to_testflight ---
[15:49:41]: ----------------------------------
[15:49:41]: Login to App Store Connect ([account name])
[15:49:43]: Login successful
[15:49:44]: Ready to upload new build to TestFlight (App: [app id])...
[15:49:44]: Fetching password for transporter from environment variable named `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`
[15:49:45]: Going to upload updated app to App Store Connect
[15:49:45]: This might take a few minutes. Please don&apos;t interrupt the script.
[15:52:53]: ------------------------------------------------------------------------------------------------------------------
[15:52:53]: --- Successfully uploaded package to App Store Connect. It might take a few minutes until it&apos;s visible online. ---
[15:52:53]: ------------------------------------------------------------------------------------------------------------------
[15:52:53]: Successfully uploaded the new binary to App Store Connect

[15:52:55]: Waiting for processing on... app_id: [...], app_version: 0.9.54, build_version: 54, platform: MAC_OS
[15:52:55]: Read more information on why this build isn&apos;t showing up yet - https://github.com/fastlane/fastlane/issues/14997
[15:52:55]: Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)
[15:53:25]: Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)
[15:53:55]: Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)
[15:54:26]: Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)
[15:55:02]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:55:33]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:56:04]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:56:35]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:57:05]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:57:37]: Waiting for App Store Connect to finish processing the new build (0.9.54 - 54) for MAC_OS
[15:58:07]: Successfully finished processing the build 0.9.54 - 54 for MAC_OS
[15:58:07]: Using App Store Connect&apos;s default for notifying external testers (which is true) - set `notify_external_testers` for full control
[15:58:07]: Distributing new build to testers: 0.9.54 - 54
[15:58:08]: Export compliance has been set to &apos;false&apos;. Need to wait for build to finishing processing again...
[15:58:08]: Set &apos;ITSAppUsesNonExemptEncryption&apos; in the &apos;Info.plist&apos; to skip this step and speed up the submission
[15:58:09]: Successfully distributed build to Internal testers &#x1F680;

+-------------------------------------------+
|             fastlane summary              |
+------+----------------------+-------------+
| Step | Action               | Time (in s) |
+------+----------------------+-------------+
| 1    | default_platform     | 0           |
| 2    | build_app            | 675         |
| 3    | upload_to_testflight | 507         |
+------+----------------------+-------------+

[15:58:09]: fastlane.tools just saved you 20 minutes! &#x1F389;</code></pre>]]></content:encoded></item><item><title><![CDATA[Flutter iOS app for MacOS (Apple Silicon)]]></title><description><![CDATA[<p>Up till now I had never bothered to look into MacOS compatibility. Scratch that, I did years ago when Flutter first became compatible with MacOS. However at the time, Google Sign In were not, so I could not have run any of my apps on MacOS anyway. This week I</p>]]></description><link>https://www.wafrat.com/flutter-ios-app-for-macos/</link><guid isPermaLink="false">68be1084021ca2165983d20a</guid><category><![CDATA[flutter]]></category><category><![CDATA[macos]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Mon, 08 Sep 2025 00:15:06 GMT</pubDate><content:encoded><![CDATA[<p>Up till now I had never bothered to look into MacOS compatibility. Scratch that, I did years ago when Flutter first became compatible with MacOS. However at the time, Google Sign In were not, so I could not have run any of my apps on MacOS anyway. This week I finally got enough time to dive into it.</p><h1 id="automatic-support-of-ios-on-macos-apple-silicon">Automatic support of iOS on MacOS (Apple Silicon)</h1><p>After I installed TestFlight on my Mac, I was surprised to see that half of my iOS apps could already be installed and run natively on it. How? And why wasn&apos;t the one I wanted to run available to run on Mac?</p><h1 id="package-compatibility">package compatibility</h1><p>At first I thought it was because it relied on packages that were not marked as being compatible with macos. For exemple <a href="https://pub.dev/packages/geocoding">https://pub.dev/packages/geocoding</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.wafrat.com/content/images/2025/09/image-6.png" class="kg-image" alt loading="lazy" width="1832" height="1092" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-6.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-6.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-6.png 1600w, https://www.wafrat.com/content/images/2025/09/image-6.png 1832w" sizes="(min-width: 720px) 720px"><figcaption>No MacOS support</figcaption></figure><p>But even after <a href="https://www.wafrat.com/reverse-geocoding-in-flutter-for-macos/">migrating away from this package</a>, and making sure all dependencies support MacOS, my app was not available in TestFlight on MacOS.</p><p>Could it be that the iOS binary is not compatible with MacOS because of some package I missed? It would be really nice to be able to know without having to blindly compile, send it to TestFlight and cross fingers.</p><h1 id="checking-flutter-side-compatibility">Checking Flutter-side compatibility</h1><p>I asked Google and apparently there are tools to do it.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-7.png" class="kg-image" alt loading="lazy" width="2000" height="1973" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-7.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-7.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-7.png 1600w, https://www.wafrat.com/content/images/2025/09/image-7.png 2070w" sizes="(min-width: 720px) 720px"></figure><p>After adding <code>pubspec_checker</code> to my dev_dependencies, I ran <code>dart run pubspec_checker ios macos</code> and got this matrix:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-9.png" class="kg-image" alt loading="lazy" width="1276" height="1146" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-9.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-9.png 1000w, https://www.wafrat.com/content/images/2025/09/image-9.png 1276w" sizes="(min-width: 720px) 720px"></figure><p>So my iOS app should totally be compatible to run on MacOS. And yet it still won&apos;t show up as being available to run in TestFlight! Could it be some XCode setting?</p><h1 id="xcode">XCode</h1><p>Even after comparing two projects, one that was compatible with MacOS and one that was not, according to TestFlight, I could not see any notable difference in settings.</p><h1 id="enabling-compatibility-in-testflight">Enabling compatibility in TestFlight</h1><p>After reading the official documentation on the subject, my app does not seem to use any feature that is specific only to iOS and wouldn&apos;t work on MacOS.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developer.apple.com/documentation/apple-silicon/running-your-ios-apps-in-macos"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Running your iOS apps in macOS | Apple Developer Documentation</div><div class="kg-bookmark-description">Modernize the iOS apps you choose to run on a Mac with Apple silicon, or opt out of running on a Mac altogether.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://developer.apple.com/apple-logo.svg" alt><span class="kg-bookmark-author">Apple Developer Documentation</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://docs.developer.apple.com/tutorials/developer-og.jpg" alt></div></a></figure><p>My app does use the device&apos;s GPS location. But apparently, even though Macs do not have a GPS, the API, Core Location, works even without a GPS.</p><blockquote>The absence of specific hardware on a Mac doesn&#x2019;t always require you to opt out. You can disable parts of your app that require device-specific hardware, allowing the user to use the rest of your app. Some technologies might also be capable of operating without specific hardware. For example, Core Location doesn&#x2019;t require GPS to return location data.</blockquote><p>Apparently, one can test an iOS app on Mac Silicon without running it in the Simulator. Neat!</p><blockquote>Xcode supports debugging, testing, and profiling your iOS app natively on a Mac with Apple silicon. When you open your iOS project in Xcode 12 or later, you have the option to build your app and run it directly in macOS. This option doesn&#x2019;t run your app in a Simulator; it runs it as an iOS App on Mac. You can then test whether your app&#x2019;s features work as expected.</blockquote><p>And finally, this is the section that was most relevant to my app: <a href="https://developer.apple.com/documentation/apple-silicon/running-your-ios-apps-in-macos#Choose-whether-to-include-your-iOS-app-in-the-Mac-App-Store">https://developer.apple.com/documentation/apple-silicon/running-your-ios-apps-in-macos#Choose-whether-to-include-your-iOS-app-in-the-Mac-App-Store</a></p><blockquote>the App Store automatically makes compatible iOS apps available to users of a Mac with Apple silicon. However, if you&#x2019;re planning to ship a macOS version of your app, or if your app doesn&#x2019;t make sense for a Mac, you can change your app&#x2019;s availability in App Store Connect.</blockquote><p>I went to App Store Connect and compared the TestFlight settings of the app that worked and the one that didn&apos;t on MacOS and sure enough, the setting for &quot;Test iPhone and iPad Apps on Apple Silicon Macs&quot; was different! After clicking Enable, the app was immediately made available on MacOS in TestFlight!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.wafrat.com/content/images/2025/09/image-10.png" class="kg-image" alt loading="lazy" width="802" height="921" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-10.png 600w, https://www.wafrat.com/content/images/2025/09/image-10.png 802w" sizes="(min-width: 720px) 720px"><figcaption>Click on `Enable`. Problem solved!</figcaption></figure><h1 id="platform-dependent-features">Platform-dependent features</h1><p>Now that I was able to run the iOS app natively on MacOS, I noticed a few problems. On a regular iOS app, I want an App Title to show what app it is. But on MacOS, the window title already states what the app is, so I don&apos;t need an App Title. I also noticed the text was slightly too small on MacOS. I wanted to scale the text.</p><p>So I tried to platform-dependent logic using <code>Platform.isMacOS</code>. Unfortunately it did not seem to work. Another Google search reveals that when running an iOS app on MacOS, <code>Platform.isMacOS</code> is <code>false</code> and <code>Platform.isIOS</code> is <code>true</code>.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-11.png" class="kg-image" alt loading="lazy" width="1810" height="1456" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-11.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-11.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-11.png 1600w, https://www.wafrat.com/content/images/2025/09/image-11.png 1810w" sizes="(min-width: 720px) 720px"></figure><p>However there are packages that help you detect if it is an iOS app running on MacOS.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/flutter_is_ios_app_on_mac"><div class="kg-bookmark-content"><div class="kg-bookmark-title">flutter_is_ios_app_on_mac | Flutter package</div><div class="kg-bookmark-description">Check if this is an iOS app executed on a mac</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/static/hash-e4t06sub/img/flutter-logo-32x32.png" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><p>That&apos;s cool, but after testing my app some more, I am more aware of some limitations which make me want to actually implement real MacOS support.</p><h1 id="limitations">Limitations</h1><p>It would take more than just a few cosmetics to make my app comfortable to use on MacOS.</p><h2 id="image-picker">Image picker</h2><p>My app can send images and the image picker I used only lets you pick among pictures in the Photos Library. On iOS it works as intended. On MacOS, I&apos;d like to be able to drop any image file onto it and upload that. I&apos;d also like a proper File Picker.</p>]]></content:encoded></item><item><title><![CDATA[Reverse geocoding in Flutter for MacOS]]></title><description><![CDATA[<p>For many years, I have been using <a href="https://pub.dev/packages/geocoding">https://pub.dev/packages/geocoding</a> successfully, but recently I&apos;ve had to migrate away from it.</p><p>This package is great because it is compatible with Android and iOS, and it uses the device&apos;s built-in reverse geocoding feature. Since it&apos;</p>]]></description><link>https://www.wafrat.com/reverse-geocoding-in-flutter-for-macos/</link><guid isPermaLink="false">68bcff4c021ca2165983d1c0</guid><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Sun, 07 Sep 2025 22:50:31 GMT</pubDate><content:encoded><![CDATA[<p>For many years, I have been using <a href="https://pub.dev/packages/geocoding">https://pub.dev/packages/geocoding</a> successfully, but recently I&apos;ve had to migrate away from it.</p><p>This package is great because it is compatible with Android and iOS, and it uses the device&apos;s built-in reverse geocoding feature. Since it&apos;s built into the device, I don&apos;t need an API key to hit the API.</p><p>However, I want to run my app as a MacOS app in MacOS and geocoding does not support it.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-2.png" class="kg-image" alt loading="lazy" width="1776" height="1444" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-2.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-2.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-2.png 1600w, https://www.wafrat.com/content/images/2025/09/image-2.png 1776w" sizes="(min-width: 720px) 720px"></figure><p>Someone filed <a href="https://github.com/Baseflow/flutter-geocoding/issues/81">a ticket</a> to the repository back in 2002 to ask for MacOS support, and someone even proposed a <a href="https://github.com/Baseflow/flutter-geocoding/pull/113">pull request</a> around the same time, but despite the package releasing updates regularly, it has been completely ignored.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-3.png" class="kg-image" alt loading="lazy" width="2000" height="2429" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-3.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-3.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-3.png 1600w, https://www.wafrat.com/content/images/2025/09/image-3.png 2376w" sizes="(min-width: 720px) 720px"></figure><p>Instead I&apos;ll use the Geocoding API on Google Cloud.</p><h1 id="enabling-the-api">Enabling the API</h1><p>Go to the Google Cloud Console, APIs, look for Maps Platform APIs. Then in credentials, you can create some API keys. <a href="https://console.cloud.google.com/google/maps-apis/credentials">https://console.cloud.google.com/google/maps-apis/credentials</a>. The API to call looks like this:</p><pre><code>https://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&amp;key=YOUR_API_KEY</code></pre><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://developers.google.com/maps/documentation/geocoding/requests-reverse-geocoding#reverse-example"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Reverse geocoding (address lookup) request and response | Geocoding API | Google for Developers</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.gstatic.com/devrel-devsite/prod/vd661722dc0bf89538e3b1471bfa72ffd39d274bea13001a4422eac953971d84d/developers/images/favicon-new.png" alt><span class="kg-bookmark-author">Google for Developers</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://developers.google.com/static/maps/images/google-maps-platform-1200x675.png" alt></div></a></figure><h1 id="googleapis">googleapis</h1><p>I thought I could use the googleapis package, which is an official package auto-generated from Google API protobufs.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/googleapis"><div class="kg-bookmark-content"><div class="kg-bookmark-title">googleapis | Dart package</div><div class="kg-bookmark-description">Auto-generated client libraries for accessing Google APIs described through the API discovery service.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><p>However, to my surprise, the geocoding API is not in there!</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-4.png" class="kg-image" alt loading="lazy" width="1058" height="1934" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-4.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-4.png 1000w, https://www.wafrat.com/content/images/2025/09/image-4.png 1058w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/documentation/googleapis/latest/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">googleapis - Dart API docs</div><div class="kg-bookmark-description">googleapis API docs, for the Dart programming language.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart API docs</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/dart-logo.svg" alt></div></a></figure><p>That means I should either find a non-official package or use the REST API manually.</p><h1 id="non-official-package">Non-official package</h1><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/google_geocoding_api"><div class="kg-bookmark-content"><div class="kg-bookmark-title">google_geocoding_api | Dart package</div><div class="kg-bookmark-description">This Package implement Google Geocoding API with default and reverse geosearch</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><p>Luckily someone already made a package that uses the REST API. They use dio to make the GET requests. <a href="https://github.com/Dimolll/google_geocoding_api/blob/e48620092049eddc558c38c1d78921985fa3829e/lib/src/api/geocoding_api.dart#L79">https://github.com/Dimolll/google_geocoding_api/blob/e48620092049eddc558c38c1d78921985fa3829e/lib/src/api/geocoding_api.dart#L79</a></p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pub.dev/packages/dio"><div class="kg-bookmark-content"><div class="kg-bookmark-title">dio | Dart package</div><div class="kg-bookmark-description">A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pub.dev/favicon.ico?hash=nk4nss8c7444fg0chird9erqef2vkhb8" alt><span class="kg-bookmark-author">Dart packages</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://pub.dev/static/hash-e4t06sub/img/pub-dev-icon-cover-image.png" alt></div></a></figure><p>I followed the README and it worked perfectly.</p>]]></content:encoded></item><item><title><![CDATA[Setting up Firebase and Google Sign-In in my old Flutter app for Web]]></title><description><![CDATA[<p>I&apos;ve been running an Flutter app for years on Android and iOS. Recently, I wanted to support Web. It should be as simple as running <code>flutter build web</code>, then using Firebase Hosting to publish it to some domain. It ended up being slightly more complicated than that.</p><h1 id="initial-set-up">Initial</h1>]]></description><link>https://www.wafrat.com/setting-up-firebase-and-google-sign-in-my-old-flutter-app-for-web/</link><guid isPermaLink="false">68bbae85021ca2165983d145</guid><category><![CDATA[flutter web]]></category><category><![CDATA[firebase]]></category><category><![CDATA[flutter]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Sat, 06 Sep 2025 06:15:06 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;ve been running an Flutter app for years on Android and iOS. Recently, I wanted to support Web. It should be as simple as running <code>flutter build web</code>, then using Firebase Hosting to publish it to some domain. It ended up being slightly more complicated than that.</p><h1 id="initial-set-up">Initial set up</h1><p>First I was quite pleased that the <code>firebase</code> CLI supports multiple logins.</p><pre><code>firebase login:add
Visit this URL on this device to log in:
https://accounts.google.com/o/oauth2/auth?...
Waiting for authentication...
&#x2714;  Success! Added account [REDACTED]</code></pre><p>then I added the hosting information.</p><pre><code>% firebase init hosting

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You&apos;re about to initialize a Firebase project in this directory:

  /[project_path]


=== Account Setup

Which account do you want to use for this project? Choose an account or add a new one now

? Please select an option:

&#x2714;  Using account: [REDACTED]

=== Project Setup

First, let&apos;s associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we&apos;ll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory:
i  Using project [PROJECT_NAME]

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build&apos;s output directory.

? What do you want to use as your public directory? build/web
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No
&#x2714;  Wrote build/web/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

&#x2714;  Firebase initialization complete!</code></pre><p>Then I build the app for Web: <code>flutter build web</code>, and finally pushed it to a beta channel. I used this doc as a reference https://firebase.google.com/docs/hosting/test-preview-deploy.</p><pre><code>% firebase hosting:channel:deploy beta
&#x2714;  hosting:channel: Channel beta has been created on site [PROJECT_NAME].

=== Deploying to &apos;[PROJECT_NAME]&apos;...

i  deploying hosting
i  hosting[PROJECT_NAME]: beginning deploy...
i  hosting[PROJECT_NAME]: found 32 files in build/web
&#x2714;  hosting[PROJECT_NAME]: file upload complete
i  hosting[PROJECT_NAME]: finalizing version...
&#x2714;  hosting[PROJECT_NAME]: version finalized
i  hosting[PROJECT_NAME]: releasing new version...
&#x2714;  hosting[PROJECT_NAME]: release complete

&#x2714;  Deploy complete!

Project Console: https://console.firebase.google.com/project/[PROJECT_ID]/overview
Hosting URL: https://[PROJECT_NAME].web.app

    &#x2714;  hosting:channel: Channel URL ([PROJECT_NAME): https://[PROJECT_NAME]--beta-[some hash].web.app [expires 2025-09-13 12:34:03]</code></pre><p>Everything worked fine except that when I loaded the page, I got a blank page and this error:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image.png" class="kg-image" alt loading="lazy" width="1354" height="326" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image.png 1000w, https://www.wafrat.com/content/images/2025/09/image.png 1354w" sizes="(min-width: 720px) 720px"></figure><h1 id="firebase-web-set-up">Firebase Web set up</h1><p>In order to read the error message, I ran the project in VSC in Chrome. And I get:</p><pre><code>errors.dart:274 Uncaught (in promise) DartError: Assertion failed: file:///Users/[username]/.pub-cache/hosted/pub.dev/firebase_core_web-3.0.0/lib/src/firebase_core_web.dart:288:11
options != null
&quot;FirebaseOptions cannot be null when creating the default app.&quot;
    at Object.throw_ [as throw] (errors.dart:274:3)
    at Object.assertFailed (errors.dart:44:3)
    at firebase_core_web.dart:288:18
    at async_patch.dart:623:19
    at async_patch.dart:648:23
    at async_patch.dart:594:19
    at _RootZone.runUnary (zone.dart:1849:54)
    at </code></pre><p>Sure enough, my initialization does not pass FirebaseOptions:</p><pre><code class="language-dart">void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  initializeTimeZones();
  await initializeDateFormatting(AppLocale);
  await Firebase.initializeApp();
  runApp(MyApp());
}
</code></pre><p>Things must have changed between the time I made the app and the latest version of the library. After following the steps at https://firebase.google.com/docs/flutter/setup?platform=ios#initialize-firebase, I get to generate <code>firebase_options.dart</code> based on my project with all the right keys.</p><p>My init code now becomes:</p><pre><code class="language-dart">await Firebase.initializeApp(
  options: DefaultFirebaseOptions.currentPlatform,
);</code></pre><p>Next!</p><h1 id="google-sign-in">Google Sign-In</h1><pre><code>Uncaught (in promise) DartError: Assertion failed: file:///Users/[username]/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+4/lib/google_sign_in_web.dart:144:9
appClientId != null
&quot;ClientID not set. Either set it on a &lt;meta name=\&quot;google-signin-client_id\&quot; content=\&quot;CLIENT_ID\&quot; /&gt; tag, or pass clientId when initializing GoogleSignIn&quot;
    at Object.throw_ [as throw] (errors.dart:274:3)
    at Object.assertFailed (errors.dart:44:3)
    at google_sign_in_web.dart:144:20
    at async_patch.dart:623:19
    at async_patch.dart:648:23
    at Object._asyncStartSync (async_patch.dart:542:3)
    at </code></pre><p>Where do I find this ClientID? Firebase Console or Google Cloud?...</p><p>A quick search says it&apos;s indeed in Google Cloud. So I went into the Cloud Console, opened my project, and on the left-hand side navigation bar, I see APIs and Services. Then in that page, I can see:</p><blockquote>Identity Toolkit API: The Google Identity Toolkit API lets you use open standards to verify a user&apos;s identity.</blockquote><p>That&apos;s the one. In that page, there&apos;s a tab called Credentials and that&apos;s where the client ids are.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/09/image-1.png" class="kg-image" alt loading="lazy" width="1724" height="1592" srcset="https://www.wafrat.com/content/images/size/w600/2025/09/image-1.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/09/image-1.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/09/image-1.png 1600w, https://www.wafrat.com/content/images/2025/09/image-1.png 1724w" sizes="(min-width: 720px) 720px"></figure><p>In my project&apos;s <code>web/index.html</code>, I add the &lt;meta&gt; tag:</p><pre><code class="language-html">&lt;meta name=&quot;google-signin-client_id&quot; content=&quot;[redacted]&quot; /&gt;</code></pre><p>When I try to sign in, I get the following error in the Console:</p><pre><code>client:74 [GSI_LOGGER-OAUTH2_CLIENT]: Checking popup closed.
client:74 [GSI_LOGGER-TOKEN_CLIENT]: Handling response. {&quot;access_token&quot;:&quot;[REDACTED]&quot;,&quot;token_type&quot;:&quot;Bearer&quot;,&quot;expires_in&quot;:3599,&quot;scope&quot;:&quot;email profile https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile&quot;,&quot;authuser&quot;:&quot;0&quot;,&quot;prompt&quot;:&quot;none&quot;}
...
[GSI_LOGGER-TOKEN_CLIENT]: Trying to set gapi client token.
client:74 [GSI_LOGGER-TOKEN_CLIENT]: The OAuth token was not passed to gapi.client, since the gapi.client library is not loaded in your page.
browser_client.dart:82  GET https://content-people.googleapis.com/v1/people/me?sources=READ_SOURCE_TYPE_PROFILE&amp;personFields=photos%2Cnames%2CemailAddresses 403 (Forbidden)
(anonymous) @ browser_client.dart:82
(anonymous) @ async_patch.dart:623
(anonymous) @ async_patch.dart:648
(anonymous) @ async_patch.dart:594
runUnary @ zone.dart:1849
handleValue @ future_impl.dart:222
handleValueCallback @ future_impl.dart:948
_propagateToListeners @ future_impl.dart:977
[_completeWithValue] @ future_impl.dart:720
(anonymous) @ future_impl.dart:804
_microtaskLoop @ schedule_microtask.dart:40
_startMicrotaskLoop @ schedule_microtask.dart:49
tear @ operations.dart:118
(anonymous) @ async_patch.dart:188
Promise.then
_scheduleImmediateWithPromise @ async_patch.dart:187
_scheduleImmediate @ async_patch.dart:160
_scheduleAsyncCallback @ schedule_microtask.dart:70
_rootScheduleMicrotask @ zone.dart:1623
scheduleMicrotask$ @ schedule_microtask.dart:150
schedule @ stream_impl.dart:645
[_addPending] @ stream_impl.dart:368
[_sendData] @ broadcast_stream_controller.dart:428
add @ broadcast_stream_controller.dart:257
[_onTokenResponse] @ gis_client.dart:183
tear @ operations.dart:118
_callDartFunctionFast1 @ js_allow_interop_patch.dart:224
ret @ js_allow_interop_patch.dart:84
Vt.i @ client:352
(anonymous) @ client:121Understand this error
errors.dart:274 Uncaught (in promise) DartError: ClientException: {
  &quot;error&quot;: {
    &quot;code&quot;: 403,
    &quot;message&quot;: &quot;People API has not been used in project [project id] before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=[project id] then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.&quot;,
    &quot;status&quot;: &quot;PERMISSION_DENIED&quot;,
    &quot;details&quot;: [
      {
        &quot;@type&quot;: &quot;type.googleapis.com/google.rpc.ErrorInfo&quot;,
        &quot;reason&quot;: &quot;SERVICE_DISABLED&quot;,
        &quot;domain&quot;: &quot;googleapis.com&quot;,
        &quot;metadata&quot;: {
          &quot;serviceTitle&quot;: &quot;People API&quot;,
          &quot;containerInfo&quot;: &quot;[project id]&quot;,
          &quot;activationUrl&quot;: &quot;https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=[project id]&quot;,
          &quot;service&quot;: &quot;people.googleapis.com&quot;,
          &quot;consumer&quot;: &quot;projects/[project id]&quot;
        }
      },
      {
        &quot;@type&quot;: &quot;type.googleapis.com/google.rpc.LocalizedMessage&quot;,
        &quot;locale&quot;: &quot;en-US&quot;,
        &quot;message&quot;: &quot;People API has not been used in project [project id] before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=[project id] then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.&quot;
      },
      {
        &quot;@type&quot;: &quot;type.googleapis.com/google.rpc.Help&quot;,
        &quot;links&quot;: [
          {
            &quot;description&quot;: &quot;Google developers console API activation&quot;,
            &quot;url&quot;: &quot;https://console.developers.google.com/apis/api/people.googleapis.com/overview?project=[project id]&quot;
          }
        ]
      }
    ]
  }
}
, uri=https://content-people.googleapis.com/v1/people/me?sources=READ_SOURCE_TYPE_PROFILE&amp;personFields=photos%2Cnames%2CemailAddresses
    at Object.throw_ [as throw] (errors.dart:274:3)
    at people.dart:146:7
    at async_patch.dart:623:19
    at async_patch.dart:648:23
    at async_patch.dart:594:19</code></pre><p>Now I&apos;m at the point where it might make sense to refer to the Google Sign-In documentation on pub.dev. I find <a href="https://pub.dev/packages/google_sign_in_web#integration">https://pub.dev/packages/google_sign_in_web#integration</a>.</p><p>But it&apos;s strange that it asks me to enable the People API, since there was never such an error on Android or iOS. Still, I enable it and try to sign in again. And it works!!</p>]]></content:encoded></item><item><title><![CDATA[Adding the `Update branch` to GitHub PRs]]></title><description><![CDATA[<p>In my repositories, my PRs didn&apos;t have the <em>Update branch</em> button. This feature is nice when someone proposes a PR that breaks because of a separate issue. So I can fix that issue separately in a PR, merge it. Finally in the other person&apos;s PR, I</p>]]></description><link>https://www.wafrat.com/adding-the-update-branch-to-github/</link><guid isPermaLink="false">688ad1b8021ca2165983d11b</guid><category><![CDATA[github]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Thu, 31 Jul 2025 02:19:33 GMT</pubDate><content:encoded><![CDATA[<p>In my repositories, my PRs didn&apos;t have the <em>Update branch</em> button. This feature is nice when someone proposes a PR that breaks because of a separate issue. So I can fix that issue separately in a PR, merge it. Finally in the other person&apos;s PR, I can click <em>Update branch</em>, <em>Rebase</em>, then Rerun the workflows.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://www.wafrat.com/content/images/2025/07/image.png" class="kg-image" alt loading="lazy" width="1740" height="434" srcset="https://www.wafrat.com/content/images/size/w600/2025/07/image.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/07/image.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/07/image.png 1600w, https://www.wafrat.com/content/images/2025/07/image.png 1740w" sizes="(min-width: 720px) 720px"><figcaption>No <em>Update branch</em> option</figcaption></figure><p>In order to show the <em>Update branch</em> option, go to the repository&apos;s Settings page, then check that box.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/07/image-1.png" class="kg-image" alt loading="lazy" width="1786" height="420" srcset="https://www.wafrat.com/content/images/size/w600/2025/07/image-1.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/07/image-1.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/07/image-1.png 1600w, https://www.wafrat.com/content/images/2025/07/image-1.png 1786w" sizes="(min-width: 720px) 720px"></figure><p>Now <em>Update branch</em> shows up.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/07/image-2.png" class="kg-image" alt loading="lazy" width="1752" height="578" srcset="https://www.wafrat.com/content/images/size/w600/2025/07/image-2.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/07/image-2.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/07/image-2.png 1600w, https://www.wafrat.com/content/images/2025/07/image-2.png 1752w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded></item><item><title><![CDATA[Scaling down my VMs]]></title><description><![CDATA[<p>I&apos;ve had custom apps running on DigitalOcean VMs for years even though it wasn&apos;t the most cost efficient nor clean way to run them.</p><p>It&apos;s high time I turned them down.</p><h1 id="moving-domains-from-io-to-dev">Moving domains from .io to .dev</h1><p>.io domains now cost $64 per year</p>]]></description><link>https://www.wafrat.com/scaling-down-my-vms/</link><guid isPermaLink="false">6791f877021ca2165983d097</guid><category><![CDATA[web development]]></category><dc:creator><![CDATA[wafrat]]></dc:creator><pubDate>Thu, 23 Jan 2025 08:48:57 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;ve had custom apps running on DigitalOcean VMs for years even though it wasn&apos;t the most cost efficient nor clean way to run them.</p><p>It&apos;s high time I turned them down.</p><h1 id="moving-domains-from-io-to-dev">Moving domains from .io to .dev</h1><p>.io domains now cost $64 per year while .dev and .com domains cost only $12 per year. So I stopped renewing my io domains. Now all my mobile apps that point to them need to be recompiled with the new domain name.</p><ol><li>Stop the renewal of the io domains on the registrars.</li><li>Set up a new DNS on the .dev domains to point to the same VM.</li><li>Go to the VM, regenerate the SSL certificates with <a href="https://certbot.eff.org/">Certbot</a>.</li><li>Change the domain name in the nginx config. Reload nginx.</li><li>Recompile my mobile apps to use the new domain.</li><li>Publish an update.</li></ol><h1 id="moving-an-apache-web-client-to-firebase-hosting">Moving an Apache web client to Firebase hosting</h1><p>The smallest DigitalOcean VM costs $7 per month to run. The web client I am using is completely static. Hence it could be hosted on Firebase hosting.</p><p>However the web client is so old that I am not confident I could recompile it from the code. I also need to check what else was on that VM so that I can back it up before destroying it.</p><p>Here&apos;s the problem: The VM is 6 years old. At the time, for security reasons, I disabled access through login/password and only allowed login through whitelisted SSH keys. Since then, I&apos;ve sold my old computers. So now I cannot access that VM on my new Mac. I had to go use the Recovery Console:</p><blockquote>Use the Recovery Console if you need to use the recovery ISO or you can&apos;t connect to your Droplet with the Droplet Console. To use the recovery console, you must enable password authentication. If necessary, you can reset your root password below.</blockquote><h2 id="recovery-console">Recovery Console</h2><p>So I reset my root password, then accessed the Recovery Console. I went into <code>~/.ssh/authorized_keys</code> and tried to add a new one. The issue? Copy/paste does not work properly in that Recovery Console!!</p><h2 id="droplet-console">Droplet Console</h2><p>DigitalOcean proposes a second way to SSH into the VM: the Droplet Console. I try running that command.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-25.png" class="kg-image" alt loading="lazy" width="1674" height="928" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-25.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-25.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/01/image-25.png 1600w, https://www.wafrat.com/content/images/2025/01/image-25.png 1674w" sizes="(min-width: 720px) 720px"></figure><p>It fails because I don&apos;t have wget installed on my machine. A Google search tells me I should run <code>brew install wget</code>. It fails with this error:</p><blockquote>Error: can&apos;t modify frozen String: &quot;The bottle needs the Xcode Command Line Tools to be installed at /Library/Developer/CommandLineTools.\nDevelopment tools provided by Xcode.app are not sufficient.\n\nYou can install the Xcode Command Line Tools, if desired, with:\n &#xA0; &#xA0;xcode-select --install\n&quot;</blockquote><p>So I run <code>xcode-select --install</code> and it decides to download the Internet.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-24.png" class="kg-image" alt loading="lazy" width="1220" height="548" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-24.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-24.png 1000w, https://www.wafrat.com/content/images/2025/01/image-24.png 1220w" sizes="(min-width: 720px) 720px"></figure><p>Then I try again:</p><pre><code>% wget -qO- https://repos-droplet.digitalocean.com/install.sh | sudo bash
Password:
Verifying machine compatibility...
bash: line 246: dmidecode: command not found

The DigitalOcean Droplet Agent is only supported on DigitalOcean machines.</code></pre><p>I was supposed to run it on the VM itself. Can&apos;t read...</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-26.png" class="kg-image" alt loading="lazy" width="1538" height="800" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-26.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-26.png 1000w, https://www.wafrat.com/content/images/2025/01/image-26.png 1538w" sizes="(min-width: 720px) 720px"></figure><p>Once installed, the option to SSH into it on the browser shows up:</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-27.png" class="kg-image" alt loading="lazy" width="2000" height="530" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-27.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-27.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/01/image-27.png 1600w, https://www.wafrat.com/content/images/2025/01/image-27.png 2060w" sizes="(min-width: 720px) 720px"></figure><p>Luckily this browser client works better than the recovery console and I was able to copy/paste my new public key into authorized_keys.</p><p>I then SSH into it properly, and back up /var/www.html:</p><pre><code>/var/www# tar czf project_name.tar.gz html</code></pre><p>Then I copy it into my Mac:</p><pre><code>scp root@[VM IP]:/var/www/project_name.tar.gz .</code></pre><h2 id="firebase-hosting">Firebase Hosting</h2><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-28.png" class="kg-image" alt loading="lazy" width="1710" height="1374" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-28.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-28.png 1000w, https://www.wafrat.com/content/images/size/w1600/2025/01/image-28.png 1600w, https://www.wafrat.com/content/images/2025/01/image-28.png 1710w" sizes="(min-width: 720px) 720px"></figure><p>Running this command throws a bunch of errors:</p><pre><code>% npm install -g firebase-tools
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: &apos;firebase-tools@13.29.2&apos;,
npm WARN EBADENGINE   required: { node: &apos;&gt;=18.0.0 || &gt;=20.0.0&apos; },
npm WARN EBADENGINE   current: { node: &apos;v16.17.0&apos;, npm: &apos;8.19.3&apos; }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: &apos;@google-cloud/cloud-sql-connector@1.6.0&apos;,
npm WARN EBADENGINE   required: { node: &apos;&gt;=18&apos; },
npm WARN EBADENGINE   current: { node: &apos;v16.17.0&apos;, npm: &apos;8.19.3&apos; }
npm WARN EBADENGINE }
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: &apos;marked@13.0.3&apos;,
npm WARN EBADENGINE   required: { node: &apos;&gt;= 18&apos; },
npm WARN EBADENGINE   current: { node: &apos;v16.17.0&apos;, npm: &apos;8.19.3&apos; }
npm WARN EBADENGINE }
...</code></pre><p>I need to switch to Node 18+:</p><pre><code>% nvm use 18
Now using node v18.18.0 (npm v10.1.0)

% npm install -g firebase-tools

added 636 packages in 5s

70 packages are looking for funding
  run `npm fund` for details</code></pre><p>The tool is pretty nice.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/image-29.png" class="kg-image" alt loading="lazy" width="1538" height="334" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/image-29.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/image-29.png 1000w, https://www.wafrat.com/content/images/2025/01/image-29.png 1538w" sizes="(min-width: 720px) 720px"></figure><p>Once set up, I can deploy the static files:</p><pre><code>% firebase deploy       

=== Deploying to &apos;project_name&apos;...

i  deploying hosting
i  hosting[project_name]: beginning deploy...
i  hosting[project_name]: found 7 files in public
&#x2714;  hosting[project_name]: file upload complete
i  hosting[project_name]: finalizing version...
&#x2714;  hosting[project_name]: version finalized
i  hosting[project_name]: releasing new version...
&#x2714;  hosting[project_name]: release complete

&#x2714;  Deploy complete!

Project Console: https://console.firebase.google.com/project/project_name/overview
Hosting URL: https://project_name.web.app
</code></pre><p>The last step is to set up the domain names correctly. When I click &quot;Add custom domain&quot;, Firebase asks me to modify the DNS settings to verify my domain ownership. Once that is done, it shows up in my list of domains where the web app is served, and next to the new domain, it says &quot;Minting certificate&quot;.</p><figure class="kg-card kg-image-card"><img src="https://www.wafrat.com/content/images/2025/01/-----------2025-01-23------5.45.31.png" class="kg-image" alt loading="lazy" width="1062" height="174" srcset="https://www.wafrat.com/content/images/size/w600/2025/01/-----------2025-01-23------5.45.31.png 600w, https://www.wafrat.com/content/images/size/w1000/2025/01/-----------2025-01-23------5.45.31.png 1000w, https://www.wafrat.com/content/images/2025/01/-----------2025-01-23------5.45.31.png 1062w" sizes="(min-width: 720px) 720px"></figure><p>Nice! Now I won&apos;t have to run certbot and generate SSL certificates myself anymore!</p><h2 id="wrong-instructions-from-firebase">Wrong instructions from Firebase</h2><p>Firebase told me to delete the previous [my_app] A field from [my_domain].dev and set up the CNAME as <code>domain: [my_app].[my_domain].dev</code> and wouldn&apos;t pass validation until I did. So after it validated, I waited a good 3 hours for the DNS to change.</p><p>After 3 hours, pinging failed. Then I googled how long Squarespace actually updates CNAME fields. I found this: <a href="https://forum.squarespace.com/topic/320695-two-days-and-cname-has-not-propagated-anywhere/">https://forum.squarespace.com/topic/320695-two-days-and-cname-has-not-propagated-anywhere/</a>. They complain it&apos;s been two days, to which someone responds that the CNAME field should only be [my_app]. So I added such a field and also verified with <code>ping [my_app].[my_domain].dev.[my_domain].dev</code> and this did indeed return. So like the last person said, Google was wrong. Wow.</p>]]></content:encoded></item></channel></rss>