<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Mirzamehdi Karimov on Medium]]></title>
        <description><![CDATA[Stories by Mirzamehdi Karimov on Medium]]></description>
        <link>https://medium.com/@mirzemehdi?source=rss-31b52dcbf3fc------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*67sqU2xhnqMHQWvF23_vGw.jpeg</url>
            <title>Stories by Mirzamehdi Karimov on Medium</title>
            <link>https://medium.com/@mirzemehdi?source=rss-31b52dcbf3fc------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 14 May 2026 07:57:49 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mirzemehdi/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to Self-Host a Ktor Backend in Kotlin Using Dokploy]]></title>
            <link>https://medium.com/@mirzemehdi/how-to-self-host-a-ktor-backend-in-kotlin-using-dokploy-2b2f25048a53?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/2b2f25048a53</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[ktor]]></category>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[self-hosted]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Sat, 29 Nov 2025 12:01:29 GMT</pubDate>
            <atom:updated>2025-11-29T13:16:29.876Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to Deploy a Ktor Kotlin Backend Using Dokploy</h3><p>Let’s say you already have a backend written in Kotlin using <a href="https://ktor.io/">Ktor</a>. You tested everything locally, it works great, and now you want to deploy it so you can use it externally in your mobile apps or web.</p><p>That was exactly my situation too.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0IGBLfi4pR0A_yZzu8tBkQ.png" /><figcaption>Deploying Ktor Kotlin Backend to your server</figcaption></figure><p>I downloaded a server template from KMP’s website (<a href="https://kmp.jetbrains.com">https://kmp.jetbrains.co</a>m), tested it locally, and everything was fine. But here’s the thing: <strong>I’m mainly a mobile developer</strong>, and I didn’t want to spend hours playing with VPS configs, Linux permissions, firewalls, Docker commands, Nginx configs, etc. I needed something super simple.</p><p>So I used <a href="https://dokploy.com/"><strong>Dokploy</strong></a> with a VPS server. <strong>Dokploy</strong> is an open-source deployment platform that gives you a <em>clean UI</em> for deploying your apps.<br>You literally click around in the browser to deploy your backend, add databases, check logs, manage environment variables, auto-deploy on push and more.</p><h3>🖥 Step 1 — Choose a VPS Server (I Used Hetzner)</h3><p>To host your backend, you need a VPS. I use <a href="https://www.hetzner.com/"><strong>Hetzner</strong></a> because it’s cheap and very reliable.</p><ol><li>Go to <strong>hetzner.com, </strong>Create a <strong>new project. </strong>Then <strong>Create Resource → Create Server. </strong>For Dokploy UI, choose <strong>at least 2GB RAM. </strong>I usually pick 4GB or 8GB RAM (cost-optimized → x86 Intel/AMD). Choose <strong>Ubuntu 24.04 </strong>(or latest) as the image.</li><li>Now you need to add an <strong>SSH key</strong> so you can connect securely to your VPS server.</li></ol><h4>Generate SSH key</h4><p>(these commands are tested in macOS)</p><pre>ssh-keygen -t ed25519 -C &quot;your_email@example.com&quot;</pre><p>You can leave the password empty if you want. I named my file name as: id_ed25519_hetzner. Now copy the public key:</p><pre>cat ~/.ssh/id_ed25519_hetzner.pub | pbcopy</pre><p>Paste the key into the SSH section when creating the VPS. Now your VPS is ready.</p><h3>🔌 Step 2 — Connect to Your VPS</h3><p>In your Hetzner dashboard, you’ll see the server’s IP.<br>Connect from you device using:</p><pre>ssh root@IP_ADDRESS -i ~/.ssh/id_ed25519_hetzner</pre><p>Replace IP_ADDRESS with your VPS IP.</p><h3>⚙️ Step 3 — Install Dokploy (and Docker)</h3><p>Once connected:</p><pre>curl -sSL https://dokploy.com/install.sh | sh</pre><p>This installs:</p><ul><li>Dokploy UI</li><li>Docker</li><li>Everything needed to deploy your Ktor app</li></ul><p>After installation, in your browser go to <strong><em>http://YOUR_VPS_IP:3000</em></strong></p><p>You’ll see the Dokploy dashboard 🎉</p><h3>📦 Step 4 — Prepare Your Ktor Backend for Deployment</h3><p>Before deploying, we need to <strong>Dockerize</strong> the project. In your build.gradle.kts, set:</p><pre>kotlin {<br>    jvmToolchain(17)<br>}</pre><h4>Add a Dockerfile</h4><p>Create a Dockerfile in the root of your project.</p><p>Paste this:</p><pre># Stage 1: Cache Gradle dependencies<br>FROM gradle:latest AS cache<br>RUN mkdir -p /home/gradle/cache_home<br>ENV GRADLE_USER_HOME=/home/gradle/cache_home<br>COPY . /home/gradle/app/<br>WORKDIR /home/gradle/app<br>RUN gradle dependencies --no-daemon<br><br># Stage 2: Build Application<br>FROM gradle:latest AS build<br>COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle<br>COPY --chown=gradle:gradle . /home/gradle/src<br>WORKDIR /home/gradle/src<br>RUN gradle buildFatJar --no-daemon<br><br># Stage 3: Create the Runtime Image<br>FROM amazoncorretto:22 AS runtime<br>EXPOSE 8080<br>RUN mkdir /app<br>COPY --from=build /home/gradle/src/server/build/libs/*.jar /app/kotlin-backend.jar<br>ENTRYPOINT [&quot;java&quot;,&quot;-jar&quot;,&quot;/app/kotlin-backend.jar&quot;]</pre><h4>What this Dockerfile does</h4><ul><li>First stage caches Gradle dependencies</li><li>Second stage builds your fat JAR</li><li>Third stage runs your Ktor backend on Java 22</li><li>It exposes port <strong>8080</strong> for your API</li></ul><h3>🧩 (Optional but recommended) Create a Compose YAML</h3><p>I always add a docker-compose.yml, even if it’s simple now, later you can add PostgreSQL, Redis, etc.</p><p>docker-compose.yml:</p><pre>version: &quot;3.9&quot;<br><br>services:<br>  kotlin-backend:<br>    build: ./<br>    container_name: kotlin-backend<br>    restart: unless-stopped<br>    ports:<br>      - &quot;8080:8080&quot;<br>    expose:<br>      - &quot;8080&quot;</pre><h3>🚀 Step 5 — Deploy with Dokploy</h3><ol><li>Go to Dokploy dashboard</li><li>Create <strong>Project -&gt; </strong>Create <strong>Service -&gt; </strong>Choose <strong>Application</strong></li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NYzkUXiV7ccs5sgQs9sH7g.png" /></figure><p>3. In Deploy Settings-&gt; Provider section connect your <strong>GitHub repository</strong> (optional but recommended). Every push to main can auto-deploy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Sfbq3aoKGevOhwuW6toFxA.png" /></figure><ol><li>In Deploy Section-&gt; <strong>Build Type</strong>, choose <strong>Dockerfile</strong></li><li>Dockerfile path: ./Dockerfile</li><li>Click deploy, or push some changes to main branch.</li><li>Now when you visit <a href="http://YOUR_SERVER_IP:8080">http://YOUR_SERVER_IP:8080</a> you should see your API response. Congrats, now your Ktor backend is live.</li></ol><p>In Dokploy, later you can easily from UI: Add your domain, Automatic HTTPS via Let’s Encrypt, Monitor logs, Restart services, Check metrics.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2b2f25048a53" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Get the Most Out of Junie in a Kotlin Multiplatform Project]]></title>
            <link>https://medium.com/@mirzemehdi/how-to-get-the-most-out-of-junie-in-a-kotlin-multiplatform-project-3ce67d235e0a?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/3ce67d235e0a</guid>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[junie]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Sun, 19 Oct 2025 18:45:55 GMT</pubDate>
            <atom:updated>2025-10-19T19:28:59.610Z</atom:updated>
            <content:encoded><![CDATA[<p>If I had to summarize this blog post in one sentence, I’d say <a href="https://www.jetbrains.com/junie/"><strong><em>Junie</em></strong></a><strong><em> </em></strong>(<em>a coding agent by JetBrains</em>) is actually really amazing to work with when building a <strong><em>Kotlin Multiplatform </em></strong>project. It helped me speed up my development a lot. But there are a few things I did along the way that made the whole process faster and the quality better. In this post, I’ll share 6 practical tips that worked for me.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eXEbFXE7wuo3MNLtYB0sWg.png" /><figcaption>Junie in Kotlin Multiplatform</figcaption></figure><p>Before talking about Junie, let me give a bit of context. I wanted to build a new <em>Kotlin/Compose Multiplatform</em> app to participate in the <a href="https://medium.com/u/8a0f3ed2baf0">RevenueCat</a> 2025 Shipaton Hackathon and compete in the Kotlin Multiplatform category. But each day, even though I had a rough idea, I still didn’t know how the app should look in terms of UI and UX. I just kept procrastinating. I think this is one of those things people don’t talk about when they say “AI doesn’t save time.” Yes, I still review the AI-generated code, and yes, it takes time to understand the logic, and yes I sometimes rewrite what AI suggested. But for me, the real time saving comes from overcoming that procrastination. As an indie developer, this makes a huge difference. Instead of overthinking everything and trying to make it perfect before I even start, I just tell the AI to “make it perfect 😄” and then I review it after. That’s where the time saving comes in. I don’t think this part gets mentioned enough when people talk about AI productivity.</p><p>Then I got an email saying I was selected as one of 20 developers to use Junie during the hackathon for free and at full power. I was like “yes, this is amazing.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*shPi4qNfQB-OjZoN4CRUYw.png" /><figcaption>email that I got access to Junie for 2 month for free during shipaton</figcaption></figure><h3>First Impressions of Junie</h3><p>When I first started using it, I noticed something interesting. It almost never produced compilation errors. About 90 percent of the time, the code it modified just worked. Of course, the functionality depends on how well you describe the task, but still, that was impressive. The downside was that it took a lot of time to complete a task. I had been using GitHub Copilot before, and that was at least twice as fast. Waiting for Junie to finish wasn’t fun.</p><p>Another thing I noticed was that when I asked it to build some UI, the result was very basic. Not ugly, but not great either. At that point I was giving very simple prompts something like “implement a home screen where users can generate some AI influencers.”</p><p>When I saw that, my initial reaction was why not build two mobile apps in parallel. While I was reviewing AI-written code for the first one, Junie could work on the second one. And that’s exactly what I did.</p><p>Here’s the livestream if you want to watch it: <a href="https://www.youtube.com/watch?v=O-_lR3pDHrM">https://www.youtube.com/watch?v=O-_lR3pDHrM</a></p><h3>What Helped Me Speed Things Up and Improve Quality</h3><h3>1. Change the default model to Claude Sonnet</h3><p>By default, Junie uses <strong><em>GPT-5</em></strong> model. In my experience, Claude Sonnet works better for code and for creative UI. This might change in the future, but for now Sonnet felt more solid to me. By changing the model, both code quality and UI/UX improved drastically.</p><p>You can change it in Settings → Junie → Models.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UVSYwPyX1X_4pyB_mR4shQ.png" /><figcaption>Default model for Junie</figcaption></figure><h3>2. Use Junie guidelines properly</h3><p>In your project directory, create a .junie/guidelines.md file and write your project guidelines there. Things like what tech stack you’re using, what architecture you prefer, and any rules you want Junie to follow. I also create a prd.md file (Product Requirements Document) where I explain what the project is about, the tech stack, the user flow, monetization, and so on.</p><p>Whenever Junie makes a mistake, I ask it to fix the issue and then I also ask it to update my guidelines based on that fix. This is one of the best tips I can give. Over time the guide gets smarter, and Junie starts delivering better code right away.</p><p>For example, in the beginning Junie kept creating empty use cases and extra layers for no reason. It was clearly following some “Clean Architecture” patterns from GitHub. For an MVP this is just overengineering and a waste of time. So I explicitly added to the guidelines: <em>“Don’t overengineer. Don’t create empty use cases unless really needed.”</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/882/1*2JxBcVLAm7RRg4dDWgB4Aw.png" /><figcaption>Junie Guidelines</figcaption></figure><p>In the picture above, you can see the guidelines and prompts I use for my projects. In the prompts directory, there are a bunch of ready-to-use prompts for generating things like product design requirement documents, detailed UI/UX flows, monetization plans, onboarding flows, and more. I just go to ChatGPT, paste one of these prompts, give my rough app idea, and it generates each document in Markdown format one by one. Then I copy and paste those new guidelines into my guides/project directory (for example prd.md, onboarding.md, paywall.md, etc.)</p><p>I added a link in the end of the blog where you can copy and paste the exact guidelines and prompts I use for my own projects.</p><h3>3. Have a separate design system module and use Compose Hot Reload</h3><p>This one made a big difference for me for 2 main reasons. When you have a separate design system module, Junie can reuse existing UI components instead of creating new ones every time. It also makes your UI more consistent and gives you the ability to change the look of the whole app more easily. If the changes are only UI related, I just tell Junie not to compile the entire project, and compile only the JVM source set, which is way faster than compiling both Android and iOS. Since I use <a href="https://github.com/JetBrains/compose-hot-reload"><strong><em>Compose Hot Reload</em></strong></a>, I can see the result immediately. I also sometimes ask Junie to create a few variations of the same UI and then I pick the one I like most.</p><h3>4. Give Structured Prompts Instead of Plain Text</h3><p>One thing that really made a difference for me is how I write prompts. When you just type a quick idea in plain text, the results are often basic. But if you give a more structured prompt, Junie performs much better.</p><p>What I usually do is go to ChatGPT and type something like:<br> <strong>“Act as a prompt engineering expert. Improve the given prompt:”</strong></p><p>Then I paste my rough idea of what I want. If needed, I also include the prd.md document for extra context. ChatGPT then turns my messy idea into a clean, well-structured prompt. After that, I just copy and paste the improved prompt into Junie’s text area, and the results are almost always better and more accurate.</p><p>This small step saves a lot of time and gives a much stronger starting point for any task.</p><h3>5. Ask Junie to compile only the Android source set</h3><p>This one is more specific to Kotlin Multiplatform. By default, after making some changes, Junie rebuilds whole project (both Android and iOS). But in most cases I don’t even touch the iOS part of the code, because most of the work happens in the common source set. So I just ask Junie to compile only Android, and I test iOS manually if needed. Since iOS builds are very slow, seeing changes on android only is at least two times faster.</p><h3>6. Start with a boilerplate</h3><p>Junie (or any AI agent) works best when it has an existing project structure to follow. Instead of prompting it in an empty project, have a boilerplate ready even just mock implementations of some classes like ViewModel, Repository, data sources or basic UI. That way:</p><ul><li>It’s easier and faster to review generated code because you understand your own project’s architecture.</li><li>It won’t implement features in random ways each time, so it will be more deterministic.</li><li>Your architecture/code stays consistent.</li></ul><p>If you don’t have your own boilerplate, you can use something like <a href="https://kappmaker.com/"><strong><em>KAppMaker</em></strong></a>, which already has reusable UI components, in-app purchases, notifications, authentication, AI guidelines and more.</p><h3>Wrapping Up</h3><p>By the end of the hackathon, I was able to build two Compose/Kotlin Multiplatform apps: <a href="https://devpost.com/software/clipugc"><strong><em>ClipUGC</em></strong></a> and <a href="https://archgee-app.web.app/"><strong><em>ArchGee</em></strong></a>. ClipUGC <strong>won</strong> <strong>5th place in the Kotlin Multiplatform category</strong>, yaaayy :). Big thanks to <strong>JetBrains</strong> for Kotlin Multiplatform technology and and for giving me the chance to try Junie and to <a href="https://medium.com/u/8a0f3ed2baf0">RevenueCat</a> for making this hackathon happen.</p><p>You can download the AI prompts and guidelines I use from <a href="https://store.kappmaker.com/buy/4d80975e-6d5a-47da-998f-fed93a7d3cda"><strong>here</strong></a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3ce67d235e0a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Use Swift Packages in Kotlin Multiplatform using Koin]]></title>
            <link>https://proandroiddev.com/how-to-use-swift-packages-in-kotlin-multiplatform-using-koin-c7d24fdbbbd7?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/c7d24fdbbbd7</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[koin]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Mon, 03 Mar 2025 13:48:10 GMT</pubDate>
            <atom:updated>2025-03-03T15:36:56.026Z</atom:updated>
            <content:encoded><![CDATA[<p>When developing Kotlin or Compose Multiplatform applications, sometimes dependencies are not supported in Kotlin Multiplatform, and we need a way to make them work. There are several ways to use Swift dependencies in Kotlin Multiplatform. If we’re lucky and the library has a CocoaPods dependency, some libraries expose Objective-C headers and can be used directly in Kotlin by including the CocoaPods dependency. But sometimes, that is not the case, and we are limited to writing Swift code only. However, we still need to provide this Swift implementation to our Kotlin Multiplatform project.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lS25_M6me30l7knx163AHw.png" /><figcaption>Using Swift Libraries in Kotlin Multiplatform using Koin</figcaption></figure><p>In this blog post, I’ll go through using Firebase Analytics as an example, but this approach can be applied to any Swift dependency.</p><h3>Step 1: Create a Common Analytics Interface</h3><p>First, we need to create a common Analytics interface:</p><pre>interface Analytics {<br>    fun logEvent(event: String, params: Map&lt;String, Any&gt;? = emptyMap())<br>    fun setEnabled(enabled: Boolean = true) //Can be used based on user consent<br><br>    companion object {<br>        const val EVENT_SCREEN_VIEW = &quot;screen_view&quot;<br>        const val PARAM_SCREEN_NAME = &quot;screen_name&quot;<br>    }<br>}</pre><p>You can also add commonly used event names here. I also like to create an extra extension function to log each screen view:</p><pre>fun Analytics.logScreenView(screenName: String, params: Map&lt;String, Any&gt;? = emptyMap()) {<br>    logEvent(<br>        event = Analytics.EVENT_SCREEN_VIEW,<br>        params = mapOf(Analytics.PARAM_SCREEN_NAME to screenName) + (params ?: emptyMap())<br>    )<br>}</pre><h3>Step 2: Implement Analytics for Each Platform</h3><h3>Android Implementation</h3><p>In the androidMain source set, create FirebaseAnalyticsImpl:</p><pre>import android.os.Bundle<br>import com.google.firebase.analytics.FirebaseAnalytics<br><br>class FirebaseAnalyticsImpl(private val firebaseAnalytics: FirebaseAnalytics) : Analytics {<br><br>    override fun logEvent(event: String, params: Map&lt;String, Any&gt;?) {<br>        val bundle = Bundle().apply {<br>            for (entry in params ?: emptyMap()) putString(entry.key, entry.value.toString())<br>        }<br>        firebaseAnalytics.logEvent(event, bundle)<br>    }<br><br>    override fun setEnabled(enabled: Boolean) {<br>        firebaseAnalytics.setAnalyticsCollectionEnabled(enabled)<br>    }<br>}</pre><h3>iOS Implementation</h3><p>Since we don’t have CocoaPods dependencies in Kotlin Multiplatform, we need to create a Swift file in the iOS project. First, make sure you add FirebaseAnalytics dependency using Swift Package Manager. Then, in iosApp, create a new Swift file called FirebaseAnalyticsImpl:</p><pre>import Foundation<br>import ComposeApp<br>import FirebaseAnalytics<br>import FirebaseCore<br><br>class FirebaseAnalyticsImpl: ComposeApp.Analytics {<br>    func logEvent(event: String, params: [String : Any]?) {<br>        var eventParams: [String: Any] = [:]<br>        params?.forEach { key, value in eventParams[key] = &quot;\(value)&quot; }<br>        Analytics.logEvent(event, parameters: eventParams)<br>    }<br><br>    func setEnabled(enabled: Bool) {<br>        Analytics.setAnalyticsCollectionEnabled(enabled)<br>    }<br>}</pre><h3>Step 3: Provide Implementations to Kotlin Multiplatform with Koin</h3><p>Koin makes dependency injection easier. I assume you’re already familiar with providing platform-specific modules using Koin in Kotlin Multiplatform. If not, check out my blog post:</p><p><a href="https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b">Achieving Platform-Specific Implementations with Koin in KMP</a></p><h3>Android Side</h3><p>In platformModule, provide the implementation as a singleton:</p><pre>internal actual val platformModule: Module = module {<br>    single { FirebaseAnalyticsImpl(firebaseAnalytics = Firebase.analytics) } bind Analytics::class<br>}</pre><h3>iOS Side</h3><p>Since Kotlin can’t directly access Swift classes, we need a factory to create instances. First, create SwiftLibDependencyFactory in iosMain:</p><pre>interface SwiftLibDependencyFactory {<br>    fun provideFirebaseAnalyticsImpl(): Analytics<br>}</pre><p>Then, in iosApp, create SwiftLibDependencyFactoryImpl.swift:</p><pre>import Foundation<br>import ComposeApp<br>import SwiftUI<br>import UIKit<br><br>class SwiftLibDependencyFactoryImpl: SwiftLibDependencyFactory {<br>    static var shared = SwiftLibDependencyFactoryImpl()<br>    <br>    func provideFirebaseAnalyticsImpl() -&gt; any Analytics {<br>        return FirebaseAnalyticsImpl()<br>    }<br>}</pre><p>Although this requires some boilerplate code, the advantage is that you only need to do this once. When adding a new Swift library, just add a function in SwiftLibDependencyFactory and provide its implementation.</p><h3>Step 4: Connect Everything in Kotlin Multiplatform</h3><p>In iosMain, add a new koin module to manage the dependency:</p><pre>internal fun swiftLibDependenciesModule(factory: SwiftLibDependencyFactory): Module = module {<br>    single { factory.provideFirebaseAnalyticsImpl() } bind Analytics::class<br>}</pre><h3>Step 5: Provide SwiftLibDependencyFactoryImpl to Koin</h3><p>In iosMain main.kt, add an extension function to provide the module to the application:</p><pre>fun KoinApplication.provideSwiftLibDependencyFactory(factory: SwiftLibDependencyFactory) =<br>    run { modules(swiftLibDependenciesModule(factory)) }</pre><p>You probably initialize dependencies in your project something similar to this:</p><pre>object AppInitializer {<br>    fun initialize(onKoinStart: KoinApplication.() -&gt; Unit) {<br>        startKoin {<br>            onKoinStart()<br>            modules(appModules)<br>        }<br>    }<br>}</pre><p>In your Swift application, call:</p><pre>AppInitializer.shared.initialize(onKoinStart: { koinApp in<br>    koinApp.provideSwiftLibDependencyFactory(<br>        factory: SwiftLibDependencyFactoryImpl.shared<br>    )<br>})</pre><h3>Step 6: Use Analytics</h3><p>Now that everything is set up, you can inject Analytics anywhere in your Kotlin code. For example, in a Composable function:</p><pre>val analytics = koinInject&lt;Analytics&gt;()<br>analytics.logEvent(event = &quot;PurchaseButtonClick&quot;)</pre><p>Make sure to call analytics.setEnabled(true) at app startup or after obtaining user consent.</p><p>This approach ensures integration of any Swift dependencies into Kotlin Multiplatform while keeping everything organized using Koin in Kotlin Multiplatform project. And if you’re using <a href="https://kappmaker.com/?utm_source=blogpost&amp;utm_medium=post&amp;utm_campaign=swift_dependency_integration"><em>KAppMaker</em></a>, the boilerplate for this is already set up for you, so you can get started even faster.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c7d24fdbbbd7" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/how-to-use-swift-packages-in-kotlin-multiplatform-using-koin-c7d24fdbbbd7">How to Use Swift Packages in Kotlin Multiplatform using Koin</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Creating Web Demos for Kotlin Multiplatform Apps]]></title>
            <link>https://proandroiddev.com/creating-web-demos-for-kotlin-multiplatform-apps-23bea697bf3e?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/23bea697bf3e</guid>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[wasm]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Tue, 17 Dec 2024 19:33:24 GMT</pubDate>
            <atom:updated>2024-12-19T03:31:35.699Z</atom:updated>
            <content:encoded><![CDATA[<h3>Creating Web Demos for Compose Multiplatform Apps</h3><p>Let’s say you’ve built a Compose Multiplatform (CMP) app that works perfectly on both Android and iOS. Now, you want to create a demo web version to increase downloads. Thanks to <a href="https://kotlinlang.org/docs/wasm-overview.html"><strong><em>Kotlin/Wasm</em></strong></a> and the <a href="https://github.com/KAppMaker/KMPDevicePreview"><strong><em>KMPDevicePreview</em></strong></a> library, we can easily make this happen. You can check out a sample web demo of the <a href="https://mirzemehdi.com/PianoSpot/"><strong>PianoSpot</strong></a> app, created by these tools.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wJDIohI9Gu4_PlMjGBHbCA.png" /><figcaption>Kotlin/Wasm + KMPDevicePreview = Demo Mobile App</figcaption></figure><p>Kotlin/Wasm allows you to create a web version of your app using the same Compose views, but there’s one issue. When you use Kotlin/Wasm, the view will be shown like it’s on a desktop or big screen. This means users won’t get a true sense of how the app will look on their phone. That’s where the KMPDevicePreview library comes in. It lets us simulate how the app will look on a specific device, making it feel more realistic.</p><p>For this demo, I’ll walk through a simple app, but the process can be applied to nearly any Compose app or UI components. There might be some challenges when applying this to every app, but don’t worry — I’ve also shared some of the challenges I faced and how I overcame them in this blog.</p><h3>Step 1: Set Up Your Project</h3><p>First, open your existing project. I’m using a simple project generated from the Kotlin Multiplatform Wizard (you can find it <a href="https://kmp.jetbrains.com/">here</a>). If your project already includes a WebAssembly (Wasm) target, you can skip the next step. If not, follow these simple steps:</p><ol><li>In your composeApp/build.gradle.kts file, add the WasmJs target:</li></ol><pre>kotlin {<br>    androidTarget {<br>        //...<br>    }<br>    //....<br><br>    @OptIn(ExperimentalWasmDsl::class)<br>    wasmJs {<br>        moduleName = &quot;composeApp&quot;<br>        browser {<br>            val rootDirPath = project.rootDir.path<br>            val projectDirPath = project.projectDir.path<br>            commonWebpackConfig {<br>                outputFileName = &quot;composeApp.js&quot;<br>                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {<br>                    static = (static ?: mutableListOf()).apply {<br>                        add(rootDirPath)<br>                        add(projectDirPath)<br>                    }<br>                }<br>            }<br>        }<br>        binaries.executable()<br>    }<br><br>  //....<br>}</pre><p>2. Then, create a wasmJsMain directory in composeApp/src. Android Studio will suggest this automatically. Inside wasmJsMain, create the kotlin and resources directories.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/866/1*QzchpyPXnruup5IOOt3MoA.png" /><figcaption>Kotlin/Wasm sourceset</figcaption></figure><p>In the kotlin directory, create a main.kt file with the following code:</p><pre>import androidx.compose.ui.ExperimentalComposeUiApi<br>import androidx.compose.ui.window.ComposeViewport<br>import kotlinx.browser.document<br><br>@OptIn(ExperimentalComposeUiApi::class)<br>fun main() {<br>    ComposeViewport(document.body!!) {<br>        App()<br>    }<br>}</pre><p>In the resources directory, create an index.html file:</p><pre>&lt;!DOCTYPE html&gt;<br>&lt;html lang=&quot;en&quot;&gt;<br>&lt;head&gt;<br>    &lt;meta charset=&quot;UTF-8&quot;&gt;<br>    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;<br>    &lt;title&gt;DemoApp&lt;/title&gt;<br>    &lt;link type=&quot;text/css&quot; rel=&quot;stylesheet&quot; href=&quot;styles.css&quot;&gt;<br>    &lt;script type=&quot;application/javascript&quot; src=&quot;composeApp.js&quot;&gt;&lt;/script&gt;<br>&lt;/head&gt;<br>&lt;body&gt;<br>&lt;/body&gt;<br>&lt;/html&gt;</pre><p>And a styles.css file:</p><pre>html, body {<br>    width: 100%;<br>    height: 100%;<br>    margin: 0;<br>    padding: 0;<br>    overflow: hidden;<br>}</pre><p>3. To run the web version of your project, run:</p><pre>./gradlew wasmJsBrowserRun</pre><p>You should see your app running in the browser, but it’s not mobile-friendly yet. We’re not just creating a web version of our app — we’re building a mobile demo version. To achieve this, the next step is to include the KMPDevicePreview library.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lhomEHSwVp23n24Li21oSw.gif" /><figcaption>Kotlin/Wasm Web version</figcaption></figure><h3>Step 2: Add KMPDevicePreview</h3><p>The KMPDevicePreview library is easy to use and works across Android, iOS, WasmJs, JS, and desktop targets. To add it, include the following dependency in your commonMain:</p><pre>implementation(&quot;com.kappmaker:kmpdevicepreview:1.0.0-alpha02&quot;)</pre><p>Check the <a href="https://github.com/kappmaker/kmpdevicepreview">GitHub Repo</a> for the latest version — at the time of writing, it’s 1.0.0-alpha02.</p><p>After adding the library, run ./gradlew kotlinUpgradeYarnLock command. Otherwise, you might see the following error: &gt; Task :kotlinStoreYarnLock FAILED.</p><h3>Step 3: Wrap Your Composable with DevicePreview</h3><p>Now, return to your main.kt file and wrap the root App composable with DeviceWithConfigurationView like this:</p><pre>DeviceWithConfigurationView {<br>    App()<br>}</pre><p>This will simulate the app as if it’s running on a device, giving users a realistic preview of what to expect before downloading:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WF63fkE6RTALQao5jJ4LLA.gif" /><figcaption>KMPDevicePreview for Demo Compose Multiplatform App</figcaption></figure><p>You can select different device sizes and configurations (portrait or landscape). This lets you show how your app will look on various device sizes.</p><h3>Advanced Configuration and Challenges I Faced</h3><p>In this basic setup, we’ve managed to create a simple demo web version of a Kotlin Compose Multiplatform app. However, creating the demo web app for <a href="https://mirzemehdi.com/PianoSpot/"><strong><em>PianoSpot</em></strong></a> wasn’t without its challenges. Below, I’ll share the advanced configurations I implemented and the challenges I encountered while building the demo web app, along with the solutions I used.</p><h4>Advanced Configuration</h4><p><strong>Custom Device Preview</strong></p><p>For previewing a specific simulated device without including the configuration view, you can use SimulatedDevicePreview. Here&#39;s how it works:</p><pre>SimulatedDevicePreview(<br>    simulatedDevice = SimulatedDevice(<br>        device = Pixel6(), // Use a predefined device or create your own<br>        configuration = DeviceConfiguration(isDarkMode = true, isPortrait = true) // Set dark mode and orientation<br>    )<br>) {<br>    // Your composable content<br>    App()<br>}</pre><p>In this example, you can either use predefined devices like Pixel6 or create a custom device by implementing the Device interface and overriding its size properties. This gives you the flexibility to test how your app looks on different devices.</p><p><strong>Theme Configuration</strong></p><p>For testing light and dark modes in the preview, use SimulatedDeviceThemeIsDark.current:</p><pre>val isDark by SimulatedDeviceThemeIsDark.current<br>MaterialTheme(colorScheme = if (isDark) darkColorScheme() else lightColorScheme()) {<br>    // Your composable content<br>    App()<br>}</pre><p>This setup allows you to test how your app behaves in both dark and light mode, ensuring that your design is consistent across different themes.</p><h4>Challenges</h4><p><strong>1. Dependencies Not Supporting Wasm Target</strong></p><p>A main challenge I faced was the lack of support for certain dependencies when targeting the wasmJs platform. Some libraries, such as Room (for local storage) and RevenueCat (for in-app purchases), do not yet support wasm. This is understandable, given that wasmJs is still in its alpha stage, and not all libraries are expected to support it.</p><p>While this could be a problem, the main goal of the demo web app is to showcase the app’s interface and interactivity, not to replicate every feature of the native app. Therefore, excluding unsupported features like local storage and in-app purchases makes sense for the web version.</p><p><strong>Solution:</strong></p><p>To address this, I came up with two possible solutions:</p><p><strong>Simple Solution:</strong></p><p>The simpler solution was to create a separate module for the web demo app. Instead of creating a new wasmJs source set in the main composeApp module, I copied the composeApp module and renamed it to webApp. This webApp module only contains a wasmJs source set and minimal dependencies—mainly composable views and navigation—enough for the demo app.</p><ul><li>In the webApp module&#39;s build.gradle.kts file, I removed all dependencies that were not wasmJs compatible, keeping only the essentials. If I needed to use a feature from another library that wasn&#39;t wasmJs compatible, I simply removed that part or displayed a message informing the user that the feature was unavailable in the web demo.</li><li>The downside to this approach is that when I update a view in the main composeApp module, I have to also update it in the webApp module. However, since the demo app is just for preview purposes, this trade-off is acceptable. You can also create a separate UI module for composables to avoid duplication, but I found that copying the composeApp module and removing unsupported parts was faster and easier.</li></ul><p><strong>Complex Solution:</strong></p><ul><li>A more complex approach would involve creating a separate module for each unsupported library, creating abstractions for those libraries, and implementing actual code for supported platforms while providing fallback messages or fake implementations for the wasmJs target. This would keep the project up-to-date with the latest changes, but I felt this approach was overly complicated and didn&#39;t provide significant benefits for the demo app.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23bea697bf3e" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/creating-web-demos-for-kotlin-multiplatform-apps-23bea697bf3e">Creating Web Demos for Kotlin Multiplatform Apps</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Launch Apps Quickly with KAppMaker’s Kotlin Multiplatform Boilerplate]]></title>
            <link>https://medium.com/@mirzemehdi/launch-apps-quickly-with-kappmakers-kotlin-multiplatform-boilerplate-decc424d8626?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/decc424d8626</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Sun, 24 Nov 2024 12:09:39 GMT</pubDate>
            <atom:updated>2024-11-24T12:09:39.203Z</atom:updated>
            <content:encoded><![CDATA[<p>Every time I had a new mobile app idea, the process started with excitement but quickly turned into frustration. I’d spend the first 5–6 days setting up the same things — authentication, notification, navigation, in-app purchases — just to get to the interesting parts. By the time I was ready to actually build the features I cared about, I’d lose motivation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UbSmyJmjKGfZSbecwwRQSw.png" /></figure><p>Sound familiar?</p><p>After 6 years of mobile development and dealing with this over and over, I decided to fix it. That’s how <a href="https://kappmaker.com/?utm_source=medium&amp;utm_medium=blog&amp;utm_campaign=kappmaker_launch"><strong>KAppMaker</strong></a> was born — a <strong><em>Kotlin Multiplatform</em></strong> &amp; <strong><em>Compose Multiplatform boilerplate</em></strong> to help developers skip the boring setup and dive straight into building native apps for both android and ios.</p><h3>Why KAppMaker?</h3><p>I built KAppMaker because I wanted a boilerplate that’s simple but powerful and flexible to change. It gives you all the essential building blocks so you can focus on the creative parts of app development.</p><p>For example, I built <a href="https://pianospot.measify.com/"><strong>PianoSpot</strong></a>, an app to find public pianos worldwide, in just <strong>5 days</strong> using KAppMaker. And here’s something crazier: I challenged myself to build and publish a fully functional Android and iOS app <strong>in just 6 hours </strong>live. It had authentication (Google and Apple Sign In), in-app purchases for premium feature, local storage for favorites, sending push notifications, requesting network request, and more. You can watch full <a href="https://www.youtube.com/watch?v=6olW6pnx0B8">video</a> where I built and published android and ios app at the same time using almost all features of KAppMaker</p><p>If I can do that, imagine how much faster you can ship your app ideas!</p><h3>What’s Included</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C46a8faU4Oup8kurNSChag.png" /></figure><p>KAppMaker comes with essentials you need to get started:</p><ul><li><strong>Authentication</strong>: Google, Apple sign In</li><li><strong>In-app purchases</strong>: Add premium features or subscriptions.</li><li><strong>Push Notifications</strong>: Send push notifications to your users.</li><li><strong>Account management</strong>: User Logout and deletion built-in.</li><li><strong>Navigation &amp; onboarding</strong>: Prebuilt components to save hours of work.</li><li><strong>Local storage</strong>: Save user preferences or favorites easily.</li><li><strong>GitHub Actions</strong>: Automate builds and publishing for Android and iOS.</li></ul><p>It’s flexible too — you can generate screens with a single script and add new features without touching the core boilerplate.</p><h3>Who’s It For?</h3><p>KAppMaker is for developers, indie creators, and startups who want to ship faster. Whether you’re building an MVP or a production-ready app, it gives you core essentials. You need to have a skill with Kotlin &amp; Compose Multiplatform technology though, it is not a no code builder.</p><h3>Ready to Build Faster?</h3><p>If you’ve ever abandoned an app idea because of all the tedious setup, <a href="https://kappmaker.com/?utm_source=medium&amp;utm_medium=blog&amp;utm_campaign=kappmaker_launch">KAppMaker</a> is for you. Check it out on <a href="https://kappmaker.com">kappmaker.com</a> and let me know what you think!</p><p>I’d love to hear your thoughts, feedback, or even feature requests.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=decc424d8626" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[KMPNotifier Update: Web, Desktop, and New Features for Kotlin Multiplatform Notifications]]></title>
            <link>https://proandroiddev.com/kmpnotifier-update-web-desktop-and-new-features-for-kotlin-multiplatform-notifications-529b489f5d9c?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/529b489f5d9c</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[push-notification]]></category>
            <category><![CDATA[firebase]]></category>
            <category><![CDATA[notifications]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Tue, 13 Aug 2024 18:05:48 GMT</pubDate>
            <atom:updated>2024-08-14T21:34:00.868Z</atom:updated>
            <content:encoded><![CDATA[<p>In my previous <a href="https://proandroiddev.com/how-to-implement-push-notification-in-kotlin-multiplatform-5006ff20f76c">blog post</a>, I introduced <a href="https://github.com/mirzemehdi/KMPNotifier/"><strong>KMPNotifier</strong></a>, a Kotlin Multiplatform library designed to simplify local and push notification implementation across Android and iOS platforms using Firebase. I’m happy to share that it has now reached almost a <strong><em>quarter-thousand stars</em></strong> on GitHub! :). Since then, KMPNotifier has improved with new features like support for Web (JavaScript (<strong><em>JS</em></strong>) and WebAssembly (<strong><em>WASM</em></strong>)) and <strong><em>Desktop</em></strong> platforms, the option to add custom notification sounds on <strong><em>Android</em></strong> and <strong><em>iOS</em></strong>, deep linking support on Android, better handling of notification permissions on initialization of the library, and new listener methods for customizing notification behaviors. It also supports Kotlin 2.0 now. In this post, I’ll walk you through these updates and show you how to make the most of the latest version of KMPNotifier.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rp9pIRPbpm9rkQKguOADCg.png" /></figure><h4>New Platforms: Web and Desktop Support</h4><p>One of the most exciting updates to KMPNotifier is the addition of web support, including JavaScript (<strong><em>JS</em></strong>) and WebAssembly (<strong><em>WASM</em></strong>) targets. This means you can now send and receive <strong><em>local</em></strong> notifications in web applications built with Kotlin Multiplatform.</p><p>Here’s how you can initialize KMPNotifier for the web:</p><pre>NotifierManager.initialize(<br>    NotificationPlatformConfiguration.Web(<br>        askNotificationPermissionOnStart = true,<br>        notificationIconPath = null //Any image icon url or put image in resources folder<br>    )<br>)</pre><p><strong><em>NOTE</em></strong><em>: For macOS users, be sure to enable notifications for your browser in the system settings to ensure that web notifications display correctly.</em></p><p>In addition to web support, KMPNotifier now also supports local notifications on Desktop platforms. Desktop initialization:</p><pre>NotifierManager.initialize(<br>   NotificationPlatformConfiguration.Desktop(<br>       notificationIconPath = composeDesktopResourcesPath() + File.separator + &quot;ic_notification.png&quot;<br>   )<br>)</pre><p>For custom notification icon on Desktop, you need to put notification icon into resources/common folder. For more information: <a href="https://github.com/JetBrains/compose-multiplatform/blob/master/tutorials/Native_distributions_and_local_execution/README.md#packaging-resources">Compose Desktop Resources</a>.</p><h4>Custom Notification Sounds</h4><p>KMPNotifier now has the ability to use custom notification sounds on both Android and iOS.</p><p><strong>Android</strong>: To set a custom notification sound, place your audio file in the res/raw directory and initialize the library with the soundUri parameter:</p><pre>val customNotificationSound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + &quot;://&quot; + &quot;YOUR_APPLICATION_PACKAGE_NAME&quot; + &quot;/&quot; + R.raw.custom_notification_sound)<br><br>NotifierManager.initialize(<br>    configuration = NotificationPlatformConfiguration.Android(<br>        notificationIconResId = R.drawable.ic_launcher_foreground,<br>        showPushNotification = true,<br>        notificationChannelData = NotificationPlatformConfiguration.Android.NotificationChannelData(<br>            soundUri = customNotificationSound.toString()<br>        )<br>    )<br>)</pre><p><strong>iOS</strong>: For iOS, add your custom sound file to the Resources directory, then specify the notificationSoundName parameter during initialization:</p><pre>NotifierManager.initialize(<br>    NotificationPlatformConfiguration.Ios(<br>        showPushNotification = true,<br>        askNotificationPermissionOnStart = true,<br>        notificationSoundName = &quot;custom_notification_sound.wav&quot;<br>    )<br>)</pre><p>If no custom sound is specified, the default notification sound will be used.</p><h4>Deep Link Support on Android</h4><p>KMPNotifier now supports deep linking on Android, allowing you to associate notifications with specific actions within your app via URIs. To utilize this feature, include a “<strong><em>URL</em></strong>” key in your notification payload data:</p><pre>notifier.notify(<br>    title = &quot;Title&quot;, <br>    body = &quot;bodyMessage&quot;, <br>    payloadData = mapOf(<br>        Notifier.KEY_URL to &quot;https://github.com/mirzemehdi/KMPNotifier/&quot;,<br>        &quot;extraKey&quot; to &quot;randomValue&quot;<br>    )<br>)</pre><p><em>KMPNotifier will automatically set the Intent’s data to the URI provided in the “URL” key, enabling deep linking within your Android app.</em></p><h4>Notification Permission Handling</h4><p>In previous versions of KMPNotifier, notification permissions were requested automatically when the app started. The latest update introduces more flexibility, allowing developers to control when these permissions are requested. To manage notification permissions manually, set askNotificationPermissionOnStart to false during initialization. You can then use the PermissionUtil class or <a href="https://github.com/icerockdev/moko-permissions"><strong>Moko Permissions</strong></a> to request permissions when needed in your app.</p><pre>val permissionUtil = NotifierManager.getPermissionUtil()<br>permissionUtil.askNotificationPermission()</pre><h4>New Listener Methods for Advanced Notification Handling</h4><p>The latest update to KMPNotifier introduces new listener methods that improve to manage notifications effectively. These methods allow for greater customization and control over how notifications are handled within your application.</p><pre>NotifierManager.addListener(object : NotifierManager.Listener {<br>    override fun onNewToken(token: String) {<br>       println(&quot;Push Notification onNewToken: $token&quot;)<br>    }<br><br>    override fun onPushNotification(title: String?, body: String?) {<br>       super.onPushNotification(title, body)<br>       println(&quot;Push Notification notification type message is received: Title: $title and Body: $body&quot;)<br>    }<br><br>    override fun onPayloadData(data: PayloadData) {<br>        super.onPayloadData(data)<br>        println(&quot;Push Notification data type message is received, payloadData: $data&quot;)<br>    }<br><br>    override fun onNotificationClicked(data: PayloadData) {<br>        super.onNotificationClicked(data)<br>        println(&quot;Notification clicked, Notification payloadData: $data&quot;)<br>    }<br>})</pre><p><strong>Note:</strong> By default, foreground push notifications are shown to the user. If you want to customize this behavior, you can set the showPushNotification value to false when initializing the library. This way, notifications will not be displayed to the user, but you can still access the notification content using the onPushNotification listener method mentioned above.</p><h4>Notification Removal Functionality</h4><p>KMPNotifier now has a support to manage notifications by removing them when needed:</p><pre>notifier.remove(notificationId) //Remove notification by id.<br>notifier.removeAll() //Remove all notifications</pre><p>Thank you all for your support and collaboration as we improve KMPNotifier! With these updates, KMPNotifier is now available for managing notifications across Android, iOS, web, and desktop platforms. Don’t forget to check out the latest updates on our <a href="https://github.com/mirzemehdi/KMPNotifier">GitHub page</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=529b489f5d9c" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/kmpnotifier-update-web-desktop-and-new-features-for-kotlin-multiplatform-notifications-529b489f5d9c">KMPNotifier Update: Web, Desktop, and New Features for Kotlin Multiplatform Notifications</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Integrating RevenueCat into Kotlin Multiplatform]]></title>
            <link>https://medium.com/@mirzemehdi/integrating-revenuecat-into-kotlin-multiplatform-465ffa47a97b?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/465ffa47a97b</guid>
            <category><![CDATA[revenuecat]]></category>
            <category><![CDATA[in-app-purchase]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Mon, 26 Feb 2024 12:03:04 GMT</pubDate>
            <atom:updated>2024-02-26T12:03:04.000Z</atom:updated>
            <content:encoded><![CDATA[<p>Most developers, like myself, aim to earn money from our apps, and there’s a great sense of satisfaction in having that first paying customer. Recently, I considered adding subscription/in-app purchase features to my Compose+Kotlin MultiPlatform project, wanting a quick implementation without dealing with complexities of subscription.</p><p>As I thought about this, I came across <a href="https://medium.com/u/8a0f3ed2baf0">RevenueCat</a>’s new Paywall UI template feature on the internet (seems like Google’s technology is now advanced enough to show relevant content based on our thoughts xD). In case you’re not familiar, <a href="https://www.revenuecat.com/"><strong><em>RevenueCat</em></strong></a> simplifies payment and subscription integration into mobile apps without needing backend development or complex implementations. The new Paywall feature makes it even easier by providing paywall templates you can use and update from the web without updating the app in the store.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JB_tsbbYqOI346M4FNsktA.gif" /><figcaption>RevenueCat implementation in FindTravelNow</figcaption></figure><p>Excited about this discovery, I thought, “Perfect! This is exactly what I’ve been looking for. It speeds things up for me without getting into UI and backend complexities related to subscriptions.” So, I decided to use it in <a href="https://github.com/mirzemehdi/FindTravelNow-KMM"><strong><em>FindTravelNow</em></strong></a>. Initially, I was a bit disappointed since it wasn’t Kotlin Multiplatform ready. But then, I realized it’s just a library with different Swift (Objective-C compatible) and Kotlin implementations. I could easily create a wrapper and use it in a Kotlin+Compose Multiplatform project.</p><p>Enough of the story; now, let’s dive into the technical aspect of integrating RevenueCat and Paywall UI in a Kotlin+Compose Multiplatform project.</p><h3>First step— RevenueCat Library Installation</h3><p>We add android and iOS dependencies of RevenueCat and RevenueCatUI in our shared module using Gradle.</p><pre>cocoapods {<br>  ios.deploymentTarget = &quot;15.0&quot;<br>  framework {<br>    baseName = &quot;shared&quot;<br>    isStatic = true<br>  }<br>  pod(&quot;RevenueCat&quot;){<br>    extraOpts += listOf(&quot;-compiler-option&quot;, &quot;-fmodules&quot;) //Extra opts is important<br>  }<br>  pod(&quot;RevenueCatUI&quot;){<br>    extraOpts += listOf(&quot;-compiler-option&quot;, &quot;-fmodules&quot;) //Extra opts is important<br>  }<br>}<br><br>sourceSets {<br>  androidMain.dependencies {<br>    implementation(&quot;com.revenuecat.purchases:purchases:7.5.2&quot;)<br>    implementation(&quot;com.revenuecat.purchases:purchases-ui:7.5.2&quot;)<br>  }<br>}</pre><p>Make sure to include RevenueCat and RevenueCatUI from XCode as well; otherwise, errors may occur. For that I used Swift Package Manager in XCode. Select File » Add Packages... and enter the repository URL <a href="https://github.com/RevenueCat/purchases-ios.git">https://github.com/RevenueCat/purchases-ios.git</a>. Select RevenueCat and RevenueCatUI from the list.</p><h3>Second Step— Purchases Core class and Functions.</h3><p>In this step, I’ll cover the main necessary classes and functions that you can copy and paste. Additionally, I’ll share a “magic blueprint” so you can follow the same pattern to implement other functions as needed. Our main goals are to cover the initialization of RevenueCat and display the Paywall view using the RevenueCatUI <strong><em>Paywall</em></strong> composable function — these are our core requirements.</p><p>The magic blueprint involves opening the RevenueCat Configuring SDK page in two tabs. In one tab, we focus on the Kotlin implementation, while in the other tab, we refer to the Objective-C implementation page. It’s as simple as that. When creating classes in the commonMain sourceSet, we maintain the same Kotlin implementation code style.</p><p><strong>Purchases commonMain class/functions</strong></p><pre>public interface Purchases {<br>    public var logLevel: LogLevel<br>    public fun configure(apiKey: String)<br><br>    public fun login(appUserId: String, onResult: (Result&lt;LogInResult&gt;) -&gt; Unit)<br>    public fun logOut(onResult: (Result&lt;CustomerInfo&gt;) -&gt; Unit)<br>}</pre><p>And we also need data classes for logging information and customer details. For this, we can create an identical version of Kotlin data classes that are available for the Android Kotlin part in commonMain. Then, in each source set, we can include Mapper functions to map RevenueCat Android and iOS data classes to our custom data class.</p><p><strong>Purchases commonMain Data classes</strong></p><pre>public data class CustomerInfo(<br>    val originalAppUserId: String,<br>    val entitlements: EntitlementInfos<br>)</pre><pre>data class LogInResult(val customerInfo: CustomerInfo, val created: Boolean)</pre><pre>public data class EntitlementInfos(val all: Map&lt;String, EntitlementInfo&gt;)<br><br>public data class EntitlementInfo(<br>    val identifier: String,<br>    val isActive: Boolean,<br>    val willRenew: Boolean,<br>    val latestPurchaseDate: Long,<br>    val originalPurchaseDate: Long,<br>    val expirationDate: Long?,<br>    val productIdentifier: String,<br>    val productPlanIdentifier: String?,<br>    val isSandbox: Boolean,<br>    val unsubscribeDetectedAt: Long?,<br>    val billingIssueDetectedAt: Long?,<br>)</pre><p><strong>Purchases implementation in Android (androidMain sourceSet)</strong></p><pre>import com.revenuecat.purchases.Purchases as RevenueCatPurchases<br>import com.revenuecat.purchases.interfaces.LogInCallback as RevenueCatLoginCallback<br>import com.revenuecat.purchases.CustomerInfo as RevenueCatCustomerInfo<br><br>internal class PurchasesImpl(private val context: Context) : Purchases {<br>    override var logLevel: LogLevel<br>        get() = RevenueCatPurchases.logLevel.asLogLevel()<br>        set(value) {<br>            RevenueCatPurchases.logLevel = value.asRevenueCatLogLevel()<br>        }<br><br>    override fun configure(apiKey: String) {<br>        RevenueCatPurchases.configure(PurchasesConfiguration.Builder(context, apiKey).build())<br>    }<br><br><br>    override fun login(appUserId: String, onResult: (Result&lt;LogInResult&gt;) -&gt; Unit) {<br>        RevenueCatPurchases.sharedInstance.logIn(appUserId, object : RevenueCatLoginCallback {<br>            override fun onError(error: PurchasesError) {<br>                onResult(Result.failure(Exception(error.message)))<br>            }<br><br>            override fun onReceived(customerInfo: RevenueCatCustomerInfo, created: Boolean) {<br>                onResult(Result.success(LogInResult(customerInfo.asCustomerInfo(), created)))<br>            }<br>        })<br>    }<br><br>    override fun logOut(onResult: (Result&lt;CustomerInfo&gt;) -&gt; Unit) {<br>        RevenueCatPurchases.sharedInstance.logOut(object : ReceiveCustomerInfoCallback {<br>            override fun onError(error: PurchasesError) {<br>                onResult(Result.failure(Exception(error.message)))<br>            }<br><br>            override fun onReceived(customerInfo: RevenueCatCustomerInfo) {<br>                onResult(Result.success(customerInfo.asCustomerInfo()))<br>            }<br><br>        })<br>    }<br>}</pre><p><strong>Purchases Mapper functions for Android<br></strong>These mapper functions will map RevenueCat Android data classes to our custom data class that we created earlier.</p><pre><br>import com.revenuecat.purchases.LogLevel as RevenueCatLogLevel<br>import com.revenuecat.purchases.CustomerInfo as RevenueCatCustomerInfo<br>import com.revenuecat.purchases.EntitlementInfos as RevenueCatEntitlementInfos<br>import com.revenuecat.purchases.EntitlementInfo as RevenueCatEntitlementInfo<br><br><br>internal fun LogLevel.asRevenueCatLogLevel(): RevenueCatLogLevel {<br>    return when (this) {<br>        LogLevel.VERBOSE -&gt; RevenueCatLogLevel.VERBOSE<br>        LogLevel.DEBUG -&gt; RevenueCatLogLevel.DEBUG<br>        LogLevel.INFO -&gt; RevenueCatLogLevel.INFO<br>        LogLevel.WARN -&gt; RevenueCatLogLevel.WARN<br>        LogLevel.ERROR -&gt; RevenueCatLogLevel.ERROR<br>    }<br><br>}<br><br>internal fun RevenueCatLogLevel.asLogLevel(): LogLevel {<br>    return when (this) {<br>        RevenueCatLogLevel.VERBOSE -&gt; LogLevel.VERBOSE<br>        RevenueCatLogLevel.DEBUG -&gt; LogLevel.DEBUG<br>        RevenueCatLogLevel.INFO -&gt; LogLevel.INFO<br>        RevenueCatLogLevel.WARN -&gt; LogLevel.WARN<br>        RevenueCatLogLevel.ERROR -&gt; LogLevel.ERROR<br>    }<br>}<br><br>internal fun RevenueCatEntitlementInfos.asEntitlementInfos(): EntitlementInfos {<br>    return EntitlementInfos(all = this.all.mapValues { entry -&gt;<br>        entry.value.asEntitlementInfo()<br>    })<br>}<br><br>internal fun RevenueCatEntitlementInfo.asEntitlementInfo(): EntitlementInfo {<br>    return EntitlementInfo(<br>        identifier = this.identifier,<br>        isActive = this.isActive,<br>        willRenew = this.willRenew,<br>        latestPurchaseDate = this.latestPurchaseDate.time,<br>        originalPurchaseDate = this.originalPurchaseDate.time,<br>        expirationDate = this.expirationDate?.time,<br>        productIdentifier = this.productIdentifier,<br>        productPlanIdentifier = this.productPlanIdentifier,<br>        isSandbox = this.isSandbox,<br>        unsubscribeDetectedAt = this.unsubscribeDetectedAt?.time,<br>        billingIssueDetectedAt = this.billingIssueDetectedAt?.time<br>    )<br>}<br><br>internal fun RevenueCatCustomerInfo.asCustomerInfo(): CustomerInfo {<br>    return CustomerInfo(<br>        originalAppUserId = this.originalAppUserId,<br>        entitlements = entitlements.asEntitlementInfos()<br>    )<br>}</pre><p><strong>Purchases implementation in iOS (iosMain sourceSet)</strong></p><pre>@OptIn(ExperimentalForeignApi::class)<br>internal class PurchasesImpl : Purchases {<br>    override var logLevel: LogLevel<br>        get() = RCPurchases.logLevel().asLogLevel()<br>        set(value) {<br>            RCPurchases.setLogLevel(value.asRevenueCatLogLevel())<br>        }<br><br>    override fun configure(apiKey: String) {<br>        RCPurchases.configureWithAPIKey(apiKey)<br>    }<br><br><br>    override fun login(appUserId: String, onResult: (Result&lt;LogInResult&gt;) -&gt; Unit) {<br>        RCPurchases.sharedPurchases()<br>            .logIn(appUserId, completionHandler = { rcCustomerInfo, created, nsError -&gt;<br>                if (rcCustomerInfo != null) onResult(<br>                    Result.success(<br>                        LogInResult(<br>                            customerInfo = rcCustomerInfo.asCustomerInfo(),<br>                            created = created<br>                        )<br>                    )<br>                )<br>                else<br>                    onResult(Result.failure(Exception(nsError?.localizedFailureReason)))<br>            })<br>    }<br><br>    override fun logOut(onResult: (Result&lt;CustomerInfo&gt;) -&gt; Unit) {<br>        RCPurchases.sharedPurchases().logOutWithCompletion { rcCustomerInfo, nsError -&gt;<br>            if (rcCustomerInfo != null) onResult(<br>                Result.success(rcCustomerInfo.asCustomerInfo())<br>            )<br>            else<br>                onResult(Result.failure(Exception(nsError?.localizedFailureReason)))<br>        }<br>    }<br><br>}</pre><p><strong>Purchases Mapper functions for iOS</strong></p><p>These mapper functions will map RevenueCat iOS data classes to our custom data class that we created earlier.</p><pre>@OptIn(ExperimentalForeignApi::class)<br>internal fun LogLevel.asRevenueCatLogLevel(): Long {<br>    return when (this) {<br>        LogLevel.VERBOSE -&gt; RCLogLevelVerbose<br>        LogLevel.DEBUG -&gt; RCLogLevelDebug<br>        LogLevel.INFO -&gt; RCLogLevelInfo<br>        LogLevel.WARN -&gt; RCLogLevelWarn<br>        LogLevel.ERROR -&gt; RCLogLevelError<br>    }<br><br>}<br><br>@OptIn(ExperimentalForeignApi::class)<br>internal fun Long.asLogLevel(): LogLevel {<br>    return when (this) {<br>        RCLogLevelVerbose -&gt; LogLevel.VERBOSE<br>        RCLogLevelDebug -&gt; LogLevel.DEBUG<br>        RCLogLevelInfo -&gt; LogLevel.INFO<br>        RCLogLevelWarn -&gt; LogLevel.WARN<br>        RCLogLevelError -&gt; LogLevel.ERROR<br>        else -&gt; LogLevel.DEBUG<br>    }<br>}<br><br>@OptIn(ExperimentalForeignApi::class)<br>internal fun RCEntitlementInfos.asEntitlementInfos(): EntitlementInfos {<br>    val entitlementInfos: Map&lt;String, EntitlementInfo&gt; = this.all().filter { entry -&gt;<br>        entry.key is String &amp;&amp; entry.value is RCEntitlementInfo<br>    }.map { entry -&gt;<br>        val key = entry.key as String<br>        val value = entry.value as RCEntitlementInfo<br>        key to value.asEntitlementInfo()<br>    }.toMap()<br><br>    return EntitlementInfos(all = entitlementInfos)<br>}<br><br>@OptIn(ExperimentalForeignApi::class)<br>internal fun RCEntitlementInfo.asEntitlementInfo(): EntitlementInfo {<br>    return EntitlementInfo(<br>        identifier = this.identifier(),<br>        isActive = this.isActive(),<br>        willRenew = this.willRenew(),<br>        latestPurchaseDate = this.latestPurchaseDate()?.timeIntervalSince1970?.toLong() ?: 0L,<br>        originalPurchaseDate = this.originalPurchaseDate()?.timeIntervalSince1970?.toLong() ?: 0L,<br>        expirationDate = this.expirationDate()?.timeIntervalSince1970?.toLong() ?: 0L,<br>        productIdentifier = this.productIdentifier(),<br>        productPlanIdentifier = this.productPlanIdentifier(),<br>        isSandbox = this.isSandbox(),<br>        unsubscribeDetectedAt = this.unsubscribeDetectedAt()?.timeIntervalSince1970?.toLong() ?: 0L,<br>        billingIssueDetectedAt = this.billingIssueDetectedAt()?.timeIntervalSince1970?.toLong()<br>            ?: 0L<br>    )<br>}<br><br>@OptIn(ExperimentalForeignApi::class)<br>public fun RCCustomerInfo.asCustomerInfo(): CustomerInfo {<br>    return CustomerInfo(<br>        originalAppUserId = originalAppUserId(),<br>        entitlements = entitlements().asEntitlementInfos()<br>    )<br>}</pre><p>Finally, we bring all implementation classes together using either just ‘<strong><em>expect/actual</em></strong>’ or you can provide it with the <a href="https://insert-koin.io/"><strong><em>Koin</em></strong></a> DI framework, as this is my personal preference. I’ve written more detailed <a href="https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b">blog post</a> about the usage of Koin in Kotlin Multiplatform that you can check out: <a href="https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b">https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b</a>.</p><h3>Third Step — Purchases UI — Paywall Composable</h3><p>Now, we’ll move on to covering the UI part, which we can utilize as a <strong><em>Composable</em></strong> function within our composable.</p><p><strong>Purchases UI in commonMain — Paywall composable</strong></p><pre>@Composable<br>public expect fun Paywall(<br>    shouldDisplayDismissButton: Boolean = true,<br>    onDismiss: () -&gt; Unit,<br>    listener: PaywallListener?<br>)<br><br>public interface PaywallListener {<br>    public fun onPurchaseStarted() {}<br>    public fun onPurchaseCompleted(customerInfo: CustomerInfo?) {}<br>    public fun onPurchaseError(error: String?) {}<br>    public fun onPurchaseCancelled() {}<br>    public fun onRestoreStarted() {}<br>    public fun onRestoreCompleted(customerInfo: CustomerInfo?) {}<br>    public fun onRestoreError(error: String?) {}<br>}</pre><p><strong>Paywall composable implementation in Android (androidMain sourceSet)</strong></p><pre>import com.revenuecat.purchases.ui.revenuecatui.Paywall as RevenueCatPaywall<br><br>@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)<br>@Composable<br>public actual fun Paywall(<br>    shouldDisplayDismissButton: Boolean,<br>    onDismiss: () -&gt; Unit,<br>    listener: PaywallListener?<br>) {<br>    RevenueCatPaywall(<br>        options = PaywallOptions.Builder(<br>            dismissRequest = onDismiss<br>        )<br>            .setListener(listener?.asRevenueCatPaywallListener())<br>            .setShouldDisplayDismissButton(shouldDisplayDismissButton)<br>            .build()<br>    )<br>}</pre><p><strong>PaywallListener Mapper for Android (androidMain)</strong></p><pre>import com.revenuecat.purchases.ui.revenuecatui.PaywallListener as RevenueCatPaywallListener<br><br><br>@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)<br>internal fun PaywallListener.asRevenueCatPaywallListener(): RevenueCatPaywallListener {<br>    return object:RevenueCatPaywallListener{<br>        override fun onPurchaseCancelled() {<br>            this@asRevenueCatPaywallListener.onPurchaseCancelled()<br>        }<br><br>        override fun onPurchaseCompleted(<br>            customerInfo: CustomerInfo,<br>            storeTransaction: StoreTransaction<br>        ) {<br>            this@asRevenueCatPaywallListener.onPurchaseCompleted(customerInfo.asCustomerInfo())<br><br>        }<br><br>        override fun onPurchaseError(error: PurchasesError) {<br>            this@asRevenueCatPaywallListener.onPurchaseError(error.message)<br>        }<br><br>        override fun onPurchaseStarted(rcPackage: Package) {<br>            this@asRevenueCatPaywallListener.onPurchaseStarted()<br>        }<br><br>        override fun onRestoreCompleted(customerInfo: CustomerInfo) {<br>            this@asRevenueCatPaywallListener.onRestoreCompleted(customerInfo.asCustomerInfo())<br>        }<br><br>        override fun onRestoreError(error: PurchasesError) {<br>            this@asRevenueCatPaywallListener.onRestoreError(error.message)<br>        }<br><br>        override fun onRestoreStarted() {<br>            this@asRevenueCatPaywallListener.onRestoreStarted()<br>        }<br>    }<br>}</pre><p><strong>Paywall composable implementation in iOS (iosMain sourceSet)</strong></p><pre>@OptIn(ExperimentalForeignApi::class)<br>@Composable<br>public actual fun Paywall(<br>    shouldDisplayDismissButton: Boolean,<br>    onDismiss: () -&gt; Unit, listener: PaywallListener?<br>) {<br><br>    val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController<br>    val controller = RCPaywallViewController(null, shouldDisplayDismissButton)<br>    controller.setDelegate(listener?.asRCPaywallViewControllerDelegate(onDismiss))<br>    if (controller.isBeingPresented().not())<br>        rootViewController?.presentViewController(controller, true, completion = {<br>            if (controller.isBeingPresented().not()) onDismiss()<br>        })<br><br>}</pre><p><strong>PaywallListener Mapper for iOS (iosMain)</strong></p><pre>@OptIn(ExperimentalForeignApi::class)<br>@Suppress(&quot;CONFLICTING_OVERLOADS&quot;)<br>internal fun PaywallListener.asRCPaywallViewControllerDelegate(onDismissed: () -&gt; Unit): RCPaywallViewControllerDelegateProtocol {<br>    return object : RCPaywallViewControllerDelegateProtocol, NSObject() {<br>        override fun paywallViewController(<br>            controller: RCPaywallViewController,<br>            didFailPurchasingWithError: NSError<br>        ) {<br>            this@asRCPaywallViewControllerDelegate.onPurchaseError(didFailPurchasingWithError.localizedFailureReason)<br>        }<br><br><br>        override fun paywallViewController(<br>            controller: RCPaywallViewController,<br>            didFailRestoringWithError: NSError<br>        ) {<br>            this@asRCPaywallViewControllerDelegate.onRestoreError(didFailRestoringWithError.localizedFailureReason)<br>        }<br><br>        override fun paywallViewController(<br>            controller: RCPaywallViewController,<br>            didFinishRestoringWithCustomerInfo: RCCustomerInfo<br>        ) {<br>            /*<br><br>            TODO for some reason here can&#39;t get any value of didFinishRestoringWithCustomerInfo, <br>            so just pass null if it is case, if not just map it. <br><br>            */<br>            this@asRCPaywallViewControllerDelegate.onRestoreCompleted(<br>                null<br>//                didFinishRestoringWithCustomerInfo.asCustomerInfo()<br>            )<br>        }<br><br>        override fun paywallViewController(<br>            controller: RCPaywallViewController,<br>            didFinishPurchasingWithCustomerInfo: RCCustomerInfo<br>        ) {<br>            this@asRCPaywallViewControllerDelegate.onPurchaseCompleted(null)<br>        }<br><br>        override fun paywallViewController(<br>            controller: RCPaywallViewController,<br>            didFinishPurchasingWithCustomerInfo: RCCustomerInfo,<br>            transaction: RCStoreTransaction?<br>        ) {<br>            this@asRCPaywallViewControllerDelegate.onPurchaseCompleted(null)<br>        }<br><br>        override fun paywallViewControllerDidCancelPurchase(controller: RCPaywallViewController) {<br>            this@asRCPaywallViewControllerDelegate.onPurchaseCancelled()<br>        }<br><br>        override fun paywallViewControllerDidStartPurchase(controller: RCPaywallViewController) {<br>            this@asRCPaywallViewControllerDelegate.onPurchaseStarted()<br>        }<br><br>        override fun paywallViewControllerWasDismissed(controller: RCPaywallViewController) {<br>            onDismissed()<br>        }<br>    }<br><br>}</pre><p>Thanks for reading until the end! I wish you a lot of success in earning money from your in-app purchases or subscriptions :D. For those who made it this far, I want to let you in on something special — I’ve introduced a <strong><em>Lifetime Premium Subscription</em></strong> in <a href="https://app.findtravelnow.com/"><strong><em>FindTravelNow</em></strong></a>, available for a limited time at just <strong><em>$5</em></strong> (think of it as a buy-me-coffee thing :D). This lifetime subscription is meant for demo and test purposes, and I plan to keep it available for one month. However, if you act quickly, you can secure a lifetime premium subscription at this exceptionally low price. Don’t miss out on this opportunity!</p><p>Additionally, there’s one more thing — I’ve also developed the <a href="https://github.com/mirzemehdi/KMPRevenueCat"><strong><em>KMPRevenueCat</em></strong></a> wrapper library . With this, you can directly use the functionalities above without having to implement all of these steps. It’s designed to simplify the integration process.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=465ffa47a97b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[KMPAuth — Kotlin Multiplatform Authentication Library]]></title>
            <link>https://medium.com/@mirzemehdi/kmpauth-kotlin-multiplatform-authentication-library-a6d23fd83cb5?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/a6d23fd83cb5</guid>
            <category><![CDATA[apple-sign-in]]></category>
            <category><![CDATA[authentication]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Sun, 21 Jan 2024 18:28:19 GMT</pubDate>
            <atom:updated>2024-01-21T18:28:19.551Z</atom:updated>
            <content:encoded><![CDATA[<h3>KMPAuth — Kotlin Multiplatform Authentication Library</h3><p>That’s how easy is to authenticate users in your Kotlin Multiplatform project using <a href="https://github.com/mirzemehdi/KMPAuth"><strong><em>KMPAuth</em></strong></a> library. <a href="https://github.com/mirzemehdi/KMPAuth"><strong><em>KMPAuth</em></strong></a> is simple and easy to use Kotlin Multiplatform Authentication library targeting iOS and Android. Library supports <strong>Google</strong>, <strong>Apple</strong>, <strong>Github</strong> authentication integrations using Firebase.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ojzTRuzXsVVKdDO3cG9DOQ.png" /></figure><p>Because I am using <strong><em>KMPAuth</em></strong> in <a href="https://github.com/mirzemehdi/FindTravelNow-KMM/"><strong><em>FindTravelNow</em></strong></a> production open-source KMP project, I’ll support development of this library :).</p><p>Related blog post: <a href="https://proandroiddev.com/integrating-google-sign-in-into-kotlin-multiplatform-8381c189a891">Integrating Google Sign-In into Kotlin Multiplatform</a><br>You can check out <a href="https://mirzemehdi.github.io/KMPAuth">Documentation</a> for full library api information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rki9oARLUTw_3unqK7uG-g.png" /></figure><p>🚀 KMPAuth library just got even better with version 1.0.0!</p><p>What’s New?</p><p>✅ Implemented Google, Apple, Github authentication<br>✅ Developed new kmpauth-uihelper module that contains Google and Apple Sign-In Buttons<br>✅ Updated sample code.<br>✅ Updated Documentation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a6d23fd83cb5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Integrating Google Sign-In into Kotlin Multiplatform]]></title>
            <link>https://proandroiddev.com/integrating-google-sign-in-into-kotlin-multiplatform-8381c189a891?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/8381c189a891</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[google-sign-in]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Wed, 20 Dec 2023 21:13:31 GMT</pubDate>
            <atom:updated>2023-12-22T16:52:57.975Z</atom:updated>
            <content:encoded><![CDATA[<p>In this blog post I will share with you how to implement Google Sign-In in Kotlin Multiplatform. As we go step by step, we will start simple, and we will end with an awesome Google Sign-In for both Android and iOS platform!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*19vwZLSyeuhXpQ9iuGRuUA.png" /><figcaption>Google Sign-In Kotlin Multiplatform</figcaption></figure><p>Before we dive into the coding adventure, let’s understand that Google Sign In is a mix of UI and data layers. To illustrate, consider signing out; you can do it from the data layer or repository (but not necessarily), but signing in is strictly a UI task. Why? Well, we display Google Accounts for one-tap sign-ins, making it a UI-centric task. So our code needs to be easy to use, manageable, and adaptable to both UI and data layers. If you want to jump to the code immediately, here is the <a href="https://github.com/mirzemehdi/FindTravelNow-KMM/pull/7">pull request</a> showing how I implemented it in the FindTravelNow app. However, I strongly recommend reading it to understand the logic behind the implementation.</p><h3>First Step — Creating core class and functions</h3><p>Firstly, you need to set up OAuth 2.0 in Google Cloud Platform Console. For steps you can follow <a href="https://support.google.com/cloud/answer/6158849">this link</a>. <strong><em>Pro Easy Tip</em></strong>: If you use Firebase and enable Google Sign-In authentication in Firebase it will automatically generate <strong>OAuth client IDs </strong>for each platform, and one will be Web Client ID which will be needed for identifying signed-in users in backend server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dB3Jn9PcfPQmRzxc9k1-1A.png" /></figure><h4><strong><em>Common (commonMain sourceSet)</em></strong></h4><p>We will create one data class for holding authenticated Google User properties, and the most important one will be <strong><em>idToken</em> </strong>field which will be used to authenticate user in backend side.</p><pre>data class GoogleUser(<br>    val idToken: String,<br>    val displayName: String = &quot;&quot;,<br>    val profilePicUrl: String? = null,<br>)</pre><p>And another data class for holding required credential parameters.</p><pre>data class GoogleAuthCredentials(val serverId: String) //Web client ID</pre><p>Then we will create two interfaces, one will contain UI layer functionalities (a.k.a Sign-In ), and another will be related to Data layer.</p><pre>interface GoogleAuthUiProvider {<br><br>    /**<br>     * Opens Sign In with Google UI,<br>     * @return returns GoogleUser<br>     */<br>    suspend fun signIn(): GoogleUser?<br>}</pre><p>To ensure that the GoogleAuthUIProvider implementation is accessible only on the UI side and to leverage Compose Multiplatform for the UI, we will use the Composable annotation for returning GoogleAuthUIProvider in the second interface.</p><pre>interface GoogleAuthProvider {<br>    @Composable<br>    fun getUiProvider(): GoogleAuthUiProvider<br><br>    suspend fun signOut()<br>}</pre><p>Next step is to create platform specific implementation of these interfaces.</p><h4>Android (androidMain sourceSet)</h4><p>In build.gradle.kts, we need to add the following Google Sign-In dependencies for the androidMain dependencies.</p><pre>#Google Sign In<br>implementation(&quot;androidx.credentials:credentials:1.3.0-alpha01&quot;)<br>implementation(&quot;androidx.credentials:credentials-play-services-auth:1.3.0-alpha01&quot;)<br>implementation(&quot;com.google.android.libraries.identity.googleid:googleid:1.1.0&quot;)</pre><p>GoogleAuthUiProvider implementation -&gt;</p><pre>internal class GoogleAuthUiProviderImpl(<br>    private val activityContext: Context,<br>    private val credentialManager: CredentialManager,<br>    private val credentials: GoogleAuthCredentials,<br>) :<br>    GoogleAuthUiProvider {<br>    override suspend fun signIn(): GoogleUser? {<br>        return try {<br>            val credential = credentialManager.getCredential(<br>                context = activityContext,<br>                request = getCredentialRequest()<br>            ).credential<br>            getGoogleUserFromCredential(credential)<br>        } catch (e: GetCredentialException) {<br>            AppLogger.e(&quot;GoogleAuthUiProvider error: ${e.message}&quot;)<br>            null<br>        } catch (e: NullPointerException) {<br>            null<br>        }<br>    }<br><br>    private fun getGoogleUserFromCredential(credential: Credential): GoogleUser? {<br>        return when {<br>            credential is CustomCredential &amp;&amp; credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -&gt; {<br>                try {<br>                    val googleIdTokenCredential =<br>                        GoogleIdTokenCredential.createFrom(credential.data)<br>                    GoogleUser(<br>                        idToken = googleIdTokenCredential.idToken,<br>                        displayName = googleIdTokenCredential.displayName ?: &quot;&quot;,<br>                        profilePicUrl = googleIdTokenCredential.profilePictureUri?.toString()<br>                    )<br>                } catch (e: GoogleIdTokenParsingException) {<br>                    AppLogger.e(&quot;GoogleAuthUiProvider Received an invalid google id token response: ${e.message}&quot;)<br>                    null<br>                }<br>            }<br><br>            else -&gt; null<br>        }<br>    }<br><br>    private fun getCredentialRequest(): GetCredentialRequest {<br>        return GetCredentialRequest.Builder()<br>            .addCredentialOption(getGoogleIdOption(serverClientId = credentials.serverId))<br>            .build()<br>    }<br><br>    private fun getGoogleIdOption(serverClientId: String): GetGoogleIdOption {<br>        return GetGoogleIdOption.Builder()<br>            .setFilterByAuthorizedAccounts(false)<br>            .setAutoSelectEnabled(true)<br>            .setServerClientId(serverClientId)<br>            .build()<br>    }<br>}</pre><p>GoogleAuthProvider Implementation -&gt;</p><pre>internal class GoogleAuthProviderImpl(<br>    private val credentials: GoogleAuthCredentials,<br>    private val credentialManager: CredentialManager,<br>) : GoogleAuthProvider {<br><br>    @Composable<br>    override fun getUiProvider(): GoogleAuthUiProvider {<br>        val activityContext = LocalContext.current<br>        return GoogleAuthUiProviderImpl(<br>            activityContext = activityContext,<br>            credentialManager = credentialManager,<br>            credentials = credentials<br>        )<br>    }<br><br>    override suspend fun signOut() {<br>        credentialManager.clearCredentialState(ClearCredentialStateRequest())<br>    }<br>}</pre><h4>iOS (iosMain sourceSet)</h4><p>In iOS, you also need to add Google Sign-In dependencies. If you use CocoaPods, you can add them as shown below, or you can simply add the library using Swift Package Manager from Xcode.</p><pre>pod(&quot;GoogleSignIn&quot;)</pre><p>And add client and server IDs to the Info.plist file.</p><pre>&lt;key&gt;GIDServerClientID&lt;/key&gt;<br>&lt;string&gt;YOUR_SERVER_CLIENT_ID&lt;/string&gt;<br><br>&lt;key&gt;GIDClientID&lt;/key&gt;<br>&lt;string&gt;YOUR_IOS_CLIENT_ID&lt;/string&gt;<br>&lt;key&gt;CFBundleURLTypes&lt;/key&gt;<br>&lt;array&gt;<br>  &lt;dict&gt;<br>    &lt;key&gt;CFBundleURLSchemes&lt;/key&gt;<br>    &lt;array&gt;<br>      &lt;string&gt;YOUR_DOT_REVERSED_IOS_CLIENT_ID&lt;/string&gt;<br>    &lt;/array&gt;<br>  &lt;/dict&gt;<br>&lt;/array&gt;</pre><p>GoogleAuthUiProvider implementation -&gt;</p><pre>internal class GoogleAuthUiProviderImpl : GoogleAuthUiProvider {<br>    @OptIn(ExperimentalForeignApi::class)<br>    override suspend fun signIn(): GoogleUser? = suspendCoroutine { continutation -&gt;<br><br>        val rootViewController =<br>            UIApplication.sharedApplication.keyWindow?.rootViewController<br><br>        if (rootViewController == null) continutation.resume(null)<br>        else {<br>            GIDSignIn.sharedInstance<br>                .signInWithPresentingViewController(rootViewController) { gidSignInResult, nsError -&gt;<br>                    nsError?.let { println(&quot;Error While signing: $nsError&quot;) }<br>                    val idToken = gidSignInResult?.user?.idToken?.tokenString<br>                    val profile = gidSignInResult?.user?.profile<br>                    if (idToken != null) {<br>                        val googleUser = GoogleUser(<br>                            idToken = idToken,<br>                            displayName = profile?.name ?: &quot;&quot;,<br>                            profilePicUrl = profile?.imageURLWithDimension(320u)?.absoluteString<br>                        )<br>                        continutation.resume(googleUser)<br>                    } else continutation.resume(null)<br>                }<br><br>        }<br>    }<br>    <br>}</pre><p>GoogleAuthProvider implementation -&gt;</p><pre>internal class GoogleAuthProviderImpl :<br>    GoogleAuthProvider {<br><br>    @Composable<br>    override fun getUiProvider(): GoogleAuthUiProvider = GoogleAuthUiProviderImpl()<br><br>    @OptIn(ExperimentalForeignApi::class)<br>    override suspend fun signOut() {<br>        GIDSignIn.sharedInstance.signOut()<br>    }<br><br><br>}</pre><p>You only need the code below to implement application delegate function calls on the Swift side.</p><pre>class AppDelegate: NSObject, UIApplicationDelegate {<br><br>    func application(<br>      _ app: UIApplication,<br>      open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]<br>    ) -&gt; Bool {<br>      var handled: Bool<br><br>      handled = GIDSignIn.sharedInstance.handle(url)<br>      if handled {<br>        return true<br>      }<br><br>      // Handle other custom URL types.<br><br>      // If not handled by this app, return false.<br>      return false<br>    }<br><br><br>}<br><br>@main<br>struct iOSApp: App {<br>    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate<br>    <br>   var body: some Scene {<br>      WindowGroup {<br>            ContentView().onOpenURL(perform: { url in<br>                GIDSignIn.sharedInstance.handle(url)<br>            })<br>      }<br>   }<br>}</pre><p>Finally, we bring all implementation classes together using either just ‘<strong><em>expect/actual</em></strong>’ or you can use it with the <strong><em>Koin</em></strong> DI framework, as I did in <a href="https://app.findtravelnow.com/"><strong><em>FindTravelNow</em></strong></a>. I’ve written more detailed blog post about the usage of Koin in Kotlin Multiplatform that you can check out: <a href="https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b">https://medium.com/proandroiddev/achieving-platform-specific-implementations-with-koin-in-kmm-5cb029ba4f3b</a>.</p><h3>Second Step — Creating GoogleButtonUiContainer.</h3><p>Up to this point, our code is actually ready to use. However, we can add a little touch to make our lives easier, allowing us to use this button container across any project, handling all the heavy lifting for us. For each project, we can customize the button however we want.</p><pre>interface GoogleButtonUiContainerScope {<br>    fun onClick()<br>}<br><br>@Composable<br>fun GoogleButtonUiContainer(<br>    modifier: Modifier = Modifier,<br>    onGoogleSignInResult: (GoogleUser?) -&gt; Unit,<br>    content: @Composable GoogleButtonUiContainerScope.() -&gt; Unit,<br>) {<br>    val googleAuthProvider = koinInject&lt;GoogleAuthProvider&gt;()<br>    val googleAuthUiProvider = googleAuthProvider.getUiProvider()<br>    val coroutineScope = rememberCoroutineScope()<br>    val uiContainerScope = remember {<br>        object : GoogleButtonUiContainerScope {<br>            override fun onClick() {<br>                coroutineScope.launch {<br>                    val googleUser = googleAuthUiProvider.signIn()<br>                    onGoogleSignInResult(googleUser)<br>                }<br>            }<br>        }<br>    }<br>    Surface(<br>        modifier = modifier,<br>        content = { uiContainerScope.content() }<br>    )<br><br>}</pre><p>Then we simply delegate our button or view click to this container’s click, which will perform Google One Tap Sign-In and notify our screen about the result (our GoogleUser object). Using the ID token, we can send it to our backend server to check the authentication of the user. Finally, this is how simple it is to use it in your views.</p><pre>GoogleButtonUiContainer(onGoogleSignInResult = { googleUser -&gt;<br>        val idToken=googleUser.idToken // Send this idToken to your backend to verify<br>        signedInUser=googleUser<br>}) {<br>    Button(<br>        onClick = { this.onClick() }<br>    ) {<br>        Text(&quot;Sign-In with Google&quot;)<br>    }<br>}</pre><p>You can check out full source code in this PR changes: <a href="https://github.com/mirzemehdi/FindTravelNow-KMM/pull/7">https://github.com/mirzemehdi/FindTravelNow-KMM/pull/7</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8381c189a891" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/integrating-google-sign-in-into-kotlin-multiplatform-8381c189a891">Integrating Google Sign-In into Kotlin Multiplatform</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to implement Push Notifications in Kotlin Multiplatform]]></title>
            <link>https://proandroiddev.com/how-to-implement-push-notification-in-kotlin-multiplatform-5006ff20f76c?source=rss-31b52dcbf3fc------2</link>
            <guid isPermaLink="false">https://medium.com/p/5006ff20f76c</guid>
            <category><![CDATA[androiddev]]></category>
            <category><![CDATA[push-notification]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Mirzamehdi Karimov]]></dc:creator>
            <pubDate>Tue, 28 Nov 2023 21:24:16 GMT</pubDate>
            <atom:updated>2023-12-02T01:17:21.033Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*1h_XvHhq_H3ZwMJFLHXJ7Q.png" /><figcaption>Push Notification in Kotlin Multiplatform</figcaption></figure><p>In this blog post I will share with you how to implement push notifications using Firebase in Kotlin Multiplatform (targeting iOS and Android platforms).</p><p>Push notifications play a crucial role in keeping users engaged and informed about important updates in your Kotlin Multiplatform (KMP) applications. While Firebase provides a robust platform for handling push notifications, integrating it seamlessly across both Android and iOS platforms in a KMP project can be a challenging task since Firebase doesn’t yet support Kotlin Multiplatform. For this purpose we will use the <a href="https://github.com/mirzemehdi/KMPNotifier"><strong><em>KMPNotifier</em></strong></a> library. I developed this push notification library to serve as the bridge between your Kotlin Multiplatform codebase and the implementation of push notifications. Whether you’re aiming to send local notifications or push notifications, KMPNotifier provides a clean and efficient API for managing the entire notification lifecycle. With <a href="https://github.com/mirzemehdi/KMPNotifier"><strong><em>KMPNotifier</em></strong></a> you can listen for token changes, subscribe or unsubscribe to topics, get current user token, or just send local notifications for any events.</p><h4>Zeroth Step — Basic setup using Firebase official guideline</h4><p>Before starting you need to setup basic things using the Firebase official guidelines (like initializing project in Firebase, adding google-services.json to Android, and GoogleService-Info.plist to iOS).<br><em>Firebase setup for iOS — </em><a href="https://firebase.google.com/docs/android/setup">https://firebase.google.com/docs/ios/setup</a><br><em>Firebase setup for Android— </em><a href="https://firebase.google.com/docs/android/setup">https://firebase.google.com/docs/android/setup</a></p><h4>Gradle Setup</h4><p>KMPNotifier is available on Maven Central. In your root project build.gradle.kts file (or settings.gradle file) add mavenCentral() to repositories, and google-services plugin to plugins.</p><pre>plugins {<br>  id(&quot;com.android.application&quot;) version &quot;8.1.3&quot; apply false<br>  id(&quot;org.jetbrains.kotlin.multiplatform&quot;) version &quot;1.9.20&quot; apply false<br>  id(&quot;com.google.gms.google-services&quot;) version &quot;4.4.0&quot; apply false<br>}<br><br>repositories { <br>  mavenCentral()<br>}</pre><p>Then in your shared module you add dependency with latest version in commonMain. Latest version(<a href="https://github.com/mirzemehdi/KMPNotifier/releases">https://github.com/mirzemehdi/KMPNotifier/releases</a>):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/136/0*LP-Uk5_9wsrB256d" /><figcaption>Latest version</figcaption></figure><p>In iOS framework part you need to export this library as well.</p><pre>sourceSets {<br>  commonMain.dependencies {<br>    api(&quot;io.github.mirzemehdi:kmpnotifier:&lt;version&gt;&quot;) // in iOS export this library<br>  }<br>}<br>listOf(<br>        iosX64(),<br>        iosArm64(),<br>        iosSimulatorArm64()<br>    ).forEach { iosTarget -&gt;<br>        iosTarget.binaries.framework {<br>            export(project(&quot;io.github.mirzemehdi:kmpnotifier:&lt;version&gt;&quot;))<br>            baseName = &quot;shared&quot;<br>            isStatic = true<br>        }<br>    }</pre><p>And in androidApp build.gradle.kts file you apply google-services plugin</p><pre>plugins {<br>  id(&quot;com.android.application&quot;)<br>  id(&quot;com.google.gms.google-services&quot;)<br>}</pre><h4>Platform Setup</h4><p>In both platforms <strong><em>on Application Start</em></strong> you need to initialize library using <em>initialize</em> method :</p><pre>NotifierManager.initialize(NotificationPlatformConfiguration) //passing android or ios configuration depending on the platform</pre><p><strong><em>Android Setup</em></strong></p><pre>class MyApplication : Application() {<br>   override fun onCreate() {<br>       super.onCreate()<br>       NotifierManager.initialize(<br>           configuration = NotificationPlatformConfiguration.Android(<br>               notificationIconResId = R.drawable.ic_launcher_foreground,<br>           )<br>       )<br>   }<br>}</pre><p>Also starting from Android 13(API Level 33) you need to ask runtime POST_NOTIFICATIONS in activity. I created a utility function that you can use in the activity.</p><pre>val permissionUtil by permissionUtil()<br>permissionUtil.askNotificationPermission() //this will ask permission in Android 13(API Level 33) or above, otherwise permission will be granted.</pre><p><strong><em>iOS Setup</em></strong><br>First, you just need to include FirebaseMessaging library to your iOS app from Xcode. Then on application start you need to call both FirebaseApp initialization and NotifierManager initialization methods, and setting apnsToken as below. <em>Don’t forget to add Push Notifications and Background Modes (Remote Notifications) signing capability in Xcode</em>.</p><pre>import SwiftUI<br>import shared<br>import FirebaseCore<br>import FirebaseMessaging<br><br>class AppDelegate: NSObject, UIApplicationDelegate {<br>  func application(_ application: UIApplication,<br>                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -&gt; Bool {<br>      FirebaseApp.configure() //important<br>      NotifierManager.shared.initialize(configuration: NotificationPlatformConfigurationIos.shared)<br>      <br>    return true<br>  }<br>  func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {<br>        Messaging.messaging().apnsToken = deviceToken<br>  }<br>    <br>}<br>@main<br>struct iOSApp: App {<br>    <br>    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate<br>    <br>    var body: some Scene {<br>        WindowGroup {<br>            ContentView()<br>        }<br>    }<br>}</pre><h4>Usage</h4><p>You can send either local or push notification.</p><p><strong><em>Local Notification</em></strong></p><pre>val notifier = NotifierManager.getLocalNotifier()<br>notifier.notify(&quot;Title&quot;, &quot;Body&quot;) //Sends local notification</pre><p><strong><em>Push Notification</em></strong></p><p><strong><em>Listen for push notification token changes</em></strong><em> -&gt; </em>In this callback method you can send notification token to the server.</p><pre>NotifierManager.addListener(object : NotifierManager.Listener {<br>  override fun onNewToken(token: String) {<br>    println(&quot;onNewToken: $token&quot;) //Update user token in the server if needed<br>  }<br>})</pre><p><strong><em>Other useful functions</em></strong></p><pre>NotifierManager.getPushNotifier().getToken() //Get current user push notification token<br>NotifierManager.getPushNotifier().deleteMyToken() //Delete user&#39;s token for example when user logs out <br>NotifierManager.getPushNotifier().subscribeToTopic(&quot;new_users&quot;) <br>NotifierManager.getPushNotifier().unSubscribeFromTopic(&quot;new_users&quot;)</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5006ff20f76c" width="1" height="1" alt=""><hr><p><a href="https://proandroiddev.com/how-to-implement-push-notification-in-kotlin-multiplatform-5006ff20f76c">How to implement Push Notifications in Kotlin Multiplatform</a> was originally published in <a href="https://proandroiddev.com">ProAndroidDev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>