diff --git a/.editorconfig b/.editorconfig index d4903f32..6812b76d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ insert_final_newline=true indent_style=space indent_size=2 -[{*.kt, *.kts}] +[{*.kt,*.kts}] +ktlint_code_style = ktlint_official ktlint_standard_property-naming = disabled -ktlint_standard_no-wildcard-imports = disabled -ktlint_function_naming_ignore_when_annotated_with = Composable +ktlint_function_naming_ignore_when_annotated_with = Composable \ No newline at end of file diff --git a/.github/actions/setup-ci/action.yml b/.github/actions/setup-ci/action.yml new file mode 100644 index 00000000..7aec311e --- /dev/null +++ b/.github/actions/setup-ci/action.yml @@ -0,0 +1,12 @@ +name: Setup CI + +runs: + using: "composite" + steps: + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + cache: gradle + + - uses: gradle/actions/wrapper-validation@v6 diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml new file mode 100644 index 00000000..7a888483 --- /dev/null +++ b/.github/workflows/api-check.yml @@ -0,0 +1,27 @@ +name: API Check + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + api-check: + name: Check public API compatibility + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup CI + uses: ./.github/actions/setup-ci + + - name: Check API compatibility + run: ./gradlew apiCheck --no-daemon --build-cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0bbb0f9d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + +jobs: + ktlint: + name: Lint + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup CI + uses: ./.github/actions/setup-ci + + - name: Run Ktlint + run: ./gradlew ktlintCheck --no-daemon + + tests: + name: Tests + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup CI + uses: ./.github/actions/setup-ci + + - name: Run tests + run: ./gradlew test --no-daemon --build-cache diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 44212d34..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Docs - -on: - push: - branches: [ master ] - -jobs: - docs: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: fkirc/skip-duplicate-actions@master - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: 17 - - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - name: Generate docs - run: ./gradlew :library:dokkaHtml - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: docs - FOLDER: library/build/dokka/html - CLEAN: true diff --git a/.github/workflows/ktlint-check.yml b/.github/workflows/ktlint-check.yml deleted file mode 100644 index f8c484e6..00000000 --- a/.github/workflows/ktlint-check.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Ktlint Check - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - ktlint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'zulu' - - - name: Run ktlint check - run: ./gradlew ktlintCheck diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..5b056942 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish to Maven Central + +on: + workflow_dispatch: + inputs: + module: + description: Module to publish + required: true + type: choice + options: + - core + - push + - inapp + - all + + push: + branches: + - 4.0.0-rc + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + + env: + MODULE: ${{ github.event_name == 'workflow_dispatch' && inputs.module || 'all' }} + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup CI + uses: ./.github/actions/setup-ci + + - name: Verify module API + run: | + if [[ "$MODULE" == "all" ]]; then + ./gradlew apiCheck --no-daemon --stacktrace + else + ./gradlew ":$MODULE:apiCheck" --no-daemon --stacktrace + fi + + - name: Upload to Maven Central + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }} + run: | + if [[ "$MODULE" == "all" ]]; then + ./gradlew publishToMavenCentral --no-daemon --stacktrace + else + ./gradlew ":$MODULE:publishToMavenCentral" --no-daemon --stacktrace + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 4e782bd4..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Tests - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - - workflow_dispatch: - -jobs: - unit-tests: - name: Unit tests - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: fkirc/skip-duplicate-actions@master - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: 17 - - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - name: Unit tests - run: | - ./gradlew :library:testDebug --stacktrace - - api-check: - name: API changes check - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: fkirc/skip-duplicate-actions@master - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: 17 - - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} - - name: API changes check - run: | - ./gradlew :library:apiCheck --stacktrace - ./gradlew :library-no-op:apiCheck --stacktrace diff --git a/.gitignore b/.gitignore index 568843e9..5777af0a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,3 @@ captures/ .DS_Store *.iml local.properties - -sample/.idea/ diff --git a/README.md b/README.md index 3fbb4b88..1ea1fc5b 100644 --- a/README.md +++ b/README.md @@ -1,293 +1,8 @@ +# PushPushGo Android SDKs +This repository contains the Android SDKs maintained by PushPushGo. -# PushPushGo Android SDK +## SDKs -[![JitPack](https://img.shields.io/jitpack/v/github/ppgco/android-sdk?style=flat-square)](https://jitpack.io/#ppgco/android-sdk) -![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/ppgco/android-sdk/test.yml?branch=master&style=flat-square) -![GitHub tag (latest)](https://img.shields.io/github/v/tag/ppgco/android-sdk?style=flat-square) - -> [!IMPORTANT] -> **Version 3.2.0** -> -> Introducing **Live Activities (Android Live Updates)** — real-time, continuously -> updated notifications (football match tracking template) on Android 16+. -> See the [Live Activities guide](LIVE_ACTIVITIES.md). -> -> **Version 3.0.0** -> -> Introducing new **In-app messages SDK (library-inappmessages)**. -> To know more about it visit [library-inappmessages](library-inappmessages/README.md) -> -> Push notifications SDK: We recommend updating your project firebase dependencies to latest version (check below). -> - -## Requirements - -- minSdkVersion: 23 -- configured GMS or HMS in project app - -## Preparation -**Before proceeding the installlation make sure you have completed the steps listed below:** - -1. **Remove all previous implementations from other providers or custom Firebase / Huawei implementation** -2. **Connect App with Firebase / Huawei project** -3. **(GMS only) From Firebase console download google-services.json and place it in app root folder** -4. **Add dependencies based on configuration build you use - Groovy/Kotlin** - -Groovy DSL -```groovy -// /app/build.gradle -dependencies { - // GMS - implementation platform('com.google.firebase:firebase-bom:34.1.0') - implementation 'com.google.firebase:firebase-messaging' - // HMS - implementation 'com.huawei.agconnect:agconnect-core:1.9.1.303' - implementation 'com.huawei.hms:push:6.11.0.300' -} -``` - -Kotlin DSL -```kotlin -// /libs.versions.toml -[versions] -// GMS -firebaseBom = "34.1.0" -firebaseMessaging = "25.0.0" -googleGmsGoogleServices = "4.4.3" - -//HMS -agconnectCore = "1.9.1.303" -hmsPush = "6.11.0.300" - -[libraries] -// GMS -firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } -firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" } - -// HMS -agconnect-core = { module = "com.huawei.agconnect:agconnect-core", version.ref = "agconnectCore" } -hms-push = { module = "com.huawei.hms:push", version.ref = "hmsPush" } - -[plugins] -// GMS -google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" } - - - -// /app/build.gradle.kts -plugins { - alias(libs.plugins.google.gms.google.services) -} - -dependencies { - // GMS - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.messaging) - // HMS - implementation(libs.agconnect.core) - implementation(libs.hms.push) -} - - - -// /build.gradle.kts -plugins { - alias(libs.plugins.google.gms.google.services) apply false -} -``` -4. **Provide GMS / HMS credentials in PushPushGo application (/project/providers)** - -**GMS** -> * Go to your Firebase console and navigate to project settings -> * Open Cloud Messaging tab -> * Click Manage Service Accounts -> * Click on your service account email -> * Navigate to KEYS tab -> * Click ADD KEY -> * Click CREATE NEW KEY -> * Pick JSON type and click create -> * Download file and upload it in PushPushGo Application (/project/providers) in FCM v1 credentials section - -**HMS** -> * Go to your Huawei developers console -> * Navigate to your project -> * Open project settings -> * Collect required info (appId, authUrl, pushUrl, appSecret) -> * Provide credentials in PushPushGo Application (/project/providers) in HMS Provider section - -5. **In PushPushGo application collect your project ID and generate API KEY in access manager (/user/access-manager/keys) as u will need them later** - - -## Instalation - -1. **Add SDK dependency to Your project** - -Groovy DSL -```groovy -// /build.gradle -allprojects { - repositories { - // local repo - mavenLocal() - // or - // jitpack - maven { url 'https://jitpack.io' } - } -} - - -// /app/build.gradle -dependencies { - // local repo - implementation 'com.pushpushgo:sdk:' - // or - // jitpack - implementation "com.github.ppgco.android-sdk:sdk:" -} -``` -Kotlin DSL -```kotlin -// /settings.gradle.kts -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - maven ( "https://jitpack.io" ) - } -} - - - -// /libs.versions.toml -[versions] -ppgSdk = "3.1.0" - -[libraries] -ppg-sdk = { module = "com.github.ppgco.android-sdk:sdk", version.ref = "ppgSdk" } - - - -// /app/build.gradle.kts -dependencies { - implementation(libs.ppg.sdk) -} - -``` - -2. **Add to Your AndroidManifest.xml:** - -In application tag: -*Here you should pass your PPG project id and api key you have generated for that project* -```xml - - -``` - -In your main activity tag: -```xml - - - - -``` -3. **Add to your MainActivity**: - -in onCreate(): -```java -if (savedInstanceState == null) { - PushPushGo.getInstance().handleBackgroundNotificationClick(intent); -} -``` -in onNewIntent(): -```java -PushPushGo.getInstance().handleBackgroundNotificationClick(intent); -``` -4. **Add to Your Application.onCreate():** -```java -PushPushGo.getInstance(this); -``` - -5. **Configuration** -- Change default notification color: override `@color/pushpushgo_notification_color_default` -- Change default notification channel id: override `@string/pushpushgo_notification_default_channel_id` -- Change default notification channel name: override `@string/pushpushgo_notification_default_channel_name` -- Change default notification icon: override - - `res/drawable-hdpi/ic_stat_pushpushgo_default` - - `res/drawable-mdpi/ic_stat_pushpushgo_default` - - `res/drawable-xhdpi/ic_stat_pushpushgo_default` - - `res/drawable-xxhdpi/ic_stat_pushpushgo_default` - -## Usage - -- Register subscriber: -```java -PushPushGo.getInstance().registerSubscriber(); -``` - -- Unregister: -```java -PushPushGo.getInstance().unregisterSubscriber(); -``` - -- Send beacon: -```java -PushPushGo.getInstance().createBeacon() -.set("see_invoice", true) -.setCustomId("SEEI") -.appendTag("demo") -.appendTag("mobile", "platform") -.send(); -``` - -- Assign subscriber to dynamic group: -```java -PushPushGo.getInstance().createBeacon() -.assignToGroup("my-group-name") -.send(); -``` - -- Unassign subscriber from dynamic group: -```java -PushPushGo.getInstance().createBeacon() -.unassignFromGroup("my-group-name") -.send(); -``` - -- Subscribe to a Live Activity (Android 16+ live-updated notification): -```java -PushPushGo.getInstance().subscribeToLiveActivity("liveNotificationId"); -// ... -PushPushGo.getInstance().unsubscribeFromLiveActivity("liveNotificationId"); -``` -For the full integration guide (clicks, deep links, analytics, rendering -features) see [LIVE_ACTIVITIES.md](LIVE_ACTIVITIES.md). - -## Publishing - -To maven local repository: -```sh -$ ./gradlew :library:publishToMavenLocal -``` - -## Tests -Run tests in `library` module: -```sh -$ ./gradlew :library:testDebug -``` - -Generate coverage report: -```sh -$ ./gradlew :library:jacocoTestReport -``` - -HTML coverage report path: -`library/build/reports/jacocoTestReport/html/` +- **Push Notifications SDK** - [README](./push/README.md) +- **In-App Messages SDK** - [README](./inapp/README.md) diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 8430daf3..00000000 --- a/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -buildscript { - repositories { - google() - gradlePluginPortal() - maven { url 'https://developer.huawei.com/repo/' } - } - dependencies { - classpath 'com.android.tools.build:gradle:8.13.0' - classpath "org.jetbrains.dokka:dokka-core:2.1.0" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0" - classpath 'com.google.gms:google-services:4.4.4' - classpath 'com.huawei.agconnect:agcp:1.9.1.303' - } -} - -allprojects { - repositories { - google() - mavenCentral() - maven { url "https://jitpack.io" } - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } - maven { url 'https://developer.huawei.com/repo/' } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..71a7d362 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,64 @@ +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + alias(libs.plugins.android.library) apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.binary.validator) apply false + alias(libs.plugins.ktlint) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.maven.publish) apply false + id("com.huawei.agconnect") apply false +} + +buildscript { + dependencies { + classpath("com.android.tools.build:gradle:8.13.2") + classpath("com.huawei.agconnect:agcp:1.9.1.304") + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven(url = "https://developer.huawei.com/repo/") + } +} + +subprojects { + plugins.withId("com.vanniktech.maven.publish") { + extensions.configure("mavenPublishing") { + pom { + url.set("https://github.com/ppgco/android-sdk") + description.set("PushPushGo Android SDK") + inceptionYear.set("2019") + + licenses { + license { + name.set("MIT") + url.set("https://github.com/ppgco/android-sdk/blob/master/LICENSE") + } + } + + developers { + developer { + name.set("PushPushGo") + email.set("mobile-dev@pushpushgo.com") + } + } + + scm { + url.set("https://github.com/ppgco/android-sdk") + connection.set("scm:git:git://github.com/ppgco/android-sdk.git") + developerConnection.set("scm:git:ssh://github.com/ppgco/android-sdk.git") + } + } + + publishToMavenCentral(automaticRelease = false) + signAllPublications() + } + } +} diff --git a/library-inappmessages/.gitignore b/core/.gitignore similarity index 100% rename from library-inappmessages/.gitignore rename to core/.gitignore diff --git a/core/api/core.api b/core/api/core.api new file mode 100644 index 00000000..4ea0ee1f --- /dev/null +++ b/core/api/core.api @@ -0,0 +1,41 @@ +public final class com/pushpushgo/sdk/core/api/Config { + public static final field Companion Lcom/pushpushgo/sdk/core/api/Config$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun create (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/core/api/Config; + public static final fun create (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/core/api/Config; + public static final fun create (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/pushpushgo/sdk/core/api/Config; + public fun equals (Ljava/lang/Object;)Z + public final fun getApiKey ()Ljava/lang/String; + public final fun getApiUrl ()Ljava/lang/String; + public final fun getProjectId ()Ljava/lang/String; + public fun hashCode ()I + public static final fun isApiKeyFormatValid (Ljava/lang/String;)Z + public final fun isDebug ()Z + public static final fun isProjectIdFormatValid (Ljava/lang/String;)Z +} + +public final class com/pushpushgo/sdk/core/api/Config$Companion { + public final fun create (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/core/api/Config; + public final fun create (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/core/api/Config; + public final fun create (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/pushpushgo/sdk/core/api/Config; + public static synthetic fun create$default (Lcom/pushpushgo/sdk/core/api/Config$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/pushpushgo/sdk/core/api/Config; + public final fun isApiKeyFormatValid (Ljava/lang/String;)Z + public final fun isProjectIdFormatValid (Ljava/lang/String;)Z +} + +public abstract interface class com/pushpushgo/sdk/core/api/PushSubscriptionProvider { + public abstract fun getPushToken ()Ljava/lang/String; + public abstract fun isNotificationChannelEnabled ()Z + public abstract fun isSubscribed ()Z + public abstract fun subscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun unsubscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract class com/pushpushgo/sdk/core/api/PushSubscriptionProviderJavaAdapter : com/pushpushgo/sdk/core/api/PushSubscriptionProvider { + public fun ()V + public fun subscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun subscribeFuture ()Ljava/util/concurrent/CompletableFuture; + public fun unsubscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun unsubscribeFuture ()Ljava/util/concurrent/CompletableFuture; +} + diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 00000000..979a7de5 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,73 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.SourcesJar +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ktlint) + alias(libs.plugins.binary.validator) + alias(libs.plugins.maven.publish) +} + +group = "com.pushpushgo" +version = + requireNotNull(property("VERSION")) { + "VERSION property must be defined" + }.toString() + +android { + namespace = "com.pushpushgo.sdk.core" + compileSdk = 36 + + defaultConfig { + minSdk = 26 + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + languageVersion.set(KotlinVersion.KOTLIN_2_1) + apiVersion.set(KotlinVersion.KOTLIN_2_1) + } + } +} + +apiValidation { + ignoredPackages.add("com.pushpushgo.sdk.core.internal") +} + +dependencies { + implementation(libs.androidx.core.ktx) + + testImplementation(libs.junit) +} + +mavenPublishing { + coordinates(group.toString(), "sdk-core", version.toString()) + + pom { + name.set("PushPushGo SDK Core") + } + + configure( + AndroidSingleVariantLibrary( + javadocJar = JavadocJar.Empty(), + sourcesJar = SourcesJar.Sources(), + variant = "release", + ), + ) +} diff --git a/core/gradle.properties b/core/gradle.properties new file mode 100644 index 00000000..baca1705 --- /dev/null +++ b/core/gradle.properties @@ -0,0 +1 @@ +VERSION=1.0.0-SNAPSHOT diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e1000761 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/src/main/java/com/pushpushgo/sdk/core/api/Config.kt b/core/src/main/java/com/pushpushgo/sdk/core/api/Config.kt new file mode 100644 index 00000000..b45c8bf2 --- /dev/null +++ b/core/src/main/java/com/pushpushgo/sdk/core/api/Config.kt @@ -0,0 +1,57 @@ +package com.pushpushgo.sdk.core.api + +class Config private constructor( + val projectId: String, + val apiKey: String, + val apiUrl: String, + val isDebug: Boolean, +) { + companion object { + private const val API_URL = "https://api.pushpushgo.com" + private val PROJECT_ID_REGEX = Regex("^[a-z0-9]{24}$") + private val API_KEY_REGEX = Regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") + + @JvmStatic + fun isProjectIdFormatValid(projectId: String): Boolean = PROJECT_ID_REGEX.matches(projectId) + + @JvmStatic + fun isApiKeyFormatValid(apiKey: String): Boolean = API_KEY_REGEX.matches(apiKey) + + @JvmStatic + @JvmOverloads + fun create( + projectId: String, + apiKey: String, + apiUrl: String? = API_URL, + isDebug: Boolean? = false, + ): Config = Config(projectId, apiKey, apiUrl ?: API_URL, isDebug ?: false) + } + + init { + require(isProjectIdFormatValid(projectId)) { + "Invalid project ID format! - $projectId" + } + + require(isApiKeyFormatValid(apiKey)) { + "Invalid API key format" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Config) return false + + return projectId == other.projectId && + apiKey == other.apiKey && + apiUrl == other.apiUrl && + isDebug == other.isDebug + } + + override fun hashCode(): Int { + var result = projectId.hashCode() + result = 31 * result + apiKey.hashCode() + result = 31 * result + apiUrl.hashCode() + result = 31 * result + isDebug.hashCode() + return result + } +} diff --git a/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProvider.kt b/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProvider.kt new file mode 100644 index 00000000..ddf7bfca --- /dev/null +++ b/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProvider.kt @@ -0,0 +1,28 @@ +package com.pushpushgo.sdk.core.api + +interface PushSubscriptionProvider { + /** + * Subscribes to the underlying push provider. + * Must complete when registration finishes and be idempotent. + * + * A Java adapter is provided via [PushSubscriptionProviderJavaAdapter.subscribeFuture]. + */ + suspend fun subscribe() + + /** + * Unsubscribes from the underlying push provider. + * Must complete when unregistration finishes and be idempotent. + * + * A Java adapter is provided via [PushSubscriptionProviderJavaAdapter.unsubscribeFuture]. + */ + suspend fun unsubscribe() + + fun isSubscribed(): Boolean + + fun getPushToken(): String? + + /** + * Returns whether the notification channel used by underlying provider is enabled. + */ + fun isNotificationChannelEnabled(): Boolean +} diff --git a/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProviderJavaAdapter.kt b/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProviderJavaAdapter.kt new file mode 100644 index 00000000..c319dc34 --- /dev/null +++ b/core/src/main/java/com/pushpushgo/sdk/core/api/PushSubscriptionProviderJavaAdapter.kt @@ -0,0 +1,43 @@ +package com.pushpushgo.sdk.core.api + +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.concurrent.CompletableFuture +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +abstract class PushSubscriptionProviderJavaAdapter : PushSubscriptionProvider { + /** + * Subscribes to the underlying push provider. + * Must complete when registration finishes and be idempotent. + */ + abstract fun subscribeFuture(): CompletableFuture + + /** + * Unsubscribes from the underlying push provider. + * Must complete when unregistration finishes and be idempotent. + */ + abstract fun unsubscribeFuture(): CompletableFuture + + override suspend fun subscribe() { + subscribeFuture().await() + } + + override suspend fun unsubscribe() { + unsubscribeFuture().await() + } +} + +private suspend fun CompletableFuture.await(): T = + suspendCancellableCoroutine { cont -> + whenComplete { result, throwable -> + if (throwable != null) { + cont.resumeWithException(throwable) + } else { + cont.resume(result) + } + } + + cont.invokeOnCancellation { + cancel(true) + } + } diff --git a/core/src/main/java/com/pushpushgo/sdk/core/internal/ManifestConfigProvider.kt b/core/src/main/java/com/pushpushgo/sdk/core/internal/ManifestConfigProvider.kt new file mode 100644 index 00000000..2a9199a9 --- /dev/null +++ b/core/src/main/java/com/pushpushgo/sdk/core/internal/ManifestConfigProvider.kt @@ -0,0 +1,37 @@ +package com.pushpushgo.sdk.core.internal + +import android.content.Context +import android.content.pm.PackageManager +import androidx.annotation.RestrictTo +import com.pushpushgo.sdk.core.api.Config + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class ManifestConfigProvider( + private val context: Context, +) { + fun provide(): Config { + val packageManager = context.packageManager + val metadata = + packageManager + .getApplicationInfo( + context.packageName, + PackageManager.GET_META_DATA, + ).metaData + + requireNotNull(metadata) { + "Missing metadata" + } + + return Config.create( + projectId = + metadata.getString( + "com.pushpushgo.projectId", + ) ?: throw IllegalStateException("Missing metadata: com.pushpushgo.projectId"), + apiKey = + metadata.getString("com.pushpushgo.apikey") ?: metadata.getString("com.pushpushgo.apiKey") + ?: throw IllegalStateException("Missing metadata: com.pushpushgo.apikey"), + apiUrl = metadata.getString("com.pushpushgo.apiUrl"), + isDebug = metadata.getBoolean("com.pushpushgo.isDebug"), + ) + } +} diff --git a/core/src/main/java/com/pushpushgo/sdk/core/internal/NotificationPermissionProvider.kt b/core/src/main/java/com/pushpushgo/sdk/core/internal/NotificationPermissionProvider.kt new file mode 100644 index 00000000..6bf67ec7 --- /dev/null +++ b/core/src/main/java/com/pushpushgo/sdk/core/internal/NotificationPermissionProvider.kt @@ -0,0 +1,29 @@ +package com.pushpushgo.sdk.core.internal + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RestrictTo +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +object NotificationPermissionProvider { + fun canPostNotifications(context: Context): Boolean { + val manager = NotificationManagerCompat.from(context) + + if (!manager.areNotificationsEnabled()) { + return false + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true + } + + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/gradle.properties b/gradle.properties index f2dc0583..60781c37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,8 @@ -org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -XX:+UseParallelGC -kapt.include.compile.classpath=false +org.gradle.jvmargs=-Xmx2g -Dkotlin.daemon.jvm.options="-Xmx2g" -XX:+UseG1GC +org.gradle.parallel=true + kotlin.code.style=official +kotlin.explicitApi=strict + android.useAndroidX=true -android.enableJetifier=true android.nonTransitiveRClass=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..4d06ce7c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,123 @@ +[versions] +kotlin = "2.2.0" +agp = "8.13.2" +ksp = "2.2.0-2.0.2" +ktlint = "14.0.1" +binary-validator = "0.18.1" +google-services = "4.4.4" +maven-publish = "0.36.0" + +androidx-core = "1.17.0" +androidx-preference = "1.2.1" +androidx-appcompat = "1.7.1" +androidx-constraintlayout = "2.2.1" +lifecycle = "2.10.0" +navigation = "2.9.6" +work = "2.11.0" +activity-compose = "1.12.2" +emoji = "1.6.0" + +compose-bom = "2025.12.01" +compose-fonts = "1.10.0" +material = "1.13.0" + +coroutines = "1.10.2" +serialization = "1.9.0" + +retrofit = "2.11.0" +moshi = "1.15.2" +okhttp-bom = "4.12.0" +coil = "2.7.0" + +firebase-bom = "34.7.0" +hms-agconnect = "1.9.1.304" +hms-push = "6.13.0.300" +hms-update = "5.0.2.300" + +timber = "5.0.1" +json = "20251224" + +junit = "4.13.2" +mockk = "1.14.7" +espresso = "3.7.0" +androidx-junit = "1.3.0" +robolectric = "4.16" + + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } + +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +binary-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-validator" } + +google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } + + +[libraries] +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-preference = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } + +androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "navigation" } +androidx-navigation-common = { module = "androidx.navigation:navigation-common-ktx", version.ref = "navigation" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } +androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" } +androidx-work-gcm = { module = "androidx.work:work-gcm", version.ref = "work" } +androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "work" } + +androidx-emoji2 = { module = "androidx.emoji2:emoji2", version.ref = "emoji" } +androidx-emoji2-bundled = { module = "androidx.emoji2:emoji2-bundled", version.ref = "emoji" } + +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } +compose-runtime = { module = "androidx.compose.runtime:runtime" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +compose-ui-text-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "compose-fonts" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-material-icons = { module = "androidx.compose.material:material-icons-core" } + +material = { module = "com.google.android.material:material", version.ref = "material" } + +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } + +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp-bom" } +okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } + +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging" } +hms-agconnect = { module = "com.huawei.agconnect:agconnect-core", version.ref = "hms-agconnect" } +hms-push = { module = "com.huawei.hms:push", version.ref = "hms-push" } +hms-update = { module = "com.huawei.hms:update", version.ref = "hms-update" } + +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +json = { module = "org.json:json", version.ref = "json" } + +junit = { module = "junit:junit", version.ref = "junit" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } diff --git a/sample-inapp/.gitignore b/inapp/.gitignore similarity index 100% rename from sample-inapp/.gitignore rename to inapp/.gitignore diff --git a/inapp/CHANGELOG.md b/inapp/CHANGELOG.md new file mode 100644 index 00000000..73189a9e --- /dev/null +++ b/inapp/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog + +## [4.0.0] – Breaking release + +### Breaking changes + +#### Distribution +- The SDK has migrated from JitPack to Maven Central. +- The JitPack Maven repository (`https://jitpack.io`) can be removed from your Gradle configuration. + +#### SDK entry point & initialization +- **Replaced `InAppMessagesSDK` with `InAppMessages`** as the main public API. +- Initialization is now explicit and standardized: + - `InAppMessages.initialize(Application, PushSubscriptionProvider)` + - `InAppMessages.initialize(Application, Config, PushSubscriptionProvider)` + +#### Core dependency +- The SDK now depends on a shared internal **core** module. + +#### Push notifications integration +- Push subscription handling is no longer embedded in the InAppMessages SDK. +- Optional integration with push notifications is now performed via the provided `PushSubscriptionProvider` interface. + - A default implementation is provided by the PushPushGo Push Notifications SDK. + - A custom implementation may be provided by the consumer. +- The push subscription provider can no longer be changed after initialization. +- Removed legacy push integration APIs: + - `setPushNotificationSubscriber(...)` + +#### Message display API +- Removed: + - `showActiveMessages(...)` +- Introduced explicit message display APIs: + - `showMessagesOnRoute(route: String)` + - `showMessagesOnTrigger(trigger: Trigger)` +- Replaced string-based trigger handling with a strongly typed model: + - `Trigger.key(String)` + - `Trigger.keyValue(String, String)` + +#### Custom action handling +- Removed: + - `setJsActionHandler(...)` +- Introduced: + - `CustomCodeHandler` +- Optional custom action handling can now be provided during SDK initialization. +- Custom action handlers can no longer be changed after initialization. + +--- + +### Migration guide + +- This release **requires code changes** and is not source-compatible with `3.x`. +- Migrate: + - `InAppMessagesSDK` → `InAppMessages` + - Legacy push subscription handling → `PushSubscriptionProvider` + - `showActiveMessages(...)` → route- or trigger-based APIs + - String-based triggers → `Trigger` + - JS action handling → `CustomCodeHandler` passed during initialization diff --git a/library-inappmessages/README.md b/inapp/README.md similarity index 55% rename from library-inappmessages/README.md rename to inapp/README.md index 5f2c78d0..bc2c4ff4 100644 --- a/library-inappmessages/README.md +++ b/inapp/README.md @@ -1,4 +1,4 @@ -# PushPushGo In-App Messages SDK +# PushPushGo InAppMessages SDK Android SDK for integrating in-app messages into your applications. Provides advanced message management, display functionality, and user interaction based on configuration parameters provided from the backend. @@ -9,35 +9,32 @@ Android SDK for integrating in-app messages into your applications. Provides adv - [Navigation Integration](#navigation-integration) - [Triggering Messages](#triggering-messages) - [Action Handling](#action-handling) -- [Advanced Features](#advanced-features) ## Installation -To enable Pop-ups or In-app messages in your PushPushGo project, contact our support or you account manager. +To enable in-app messages in your PushPushGo project, contact our support or your account manager. -### Gradle +### Gradle setup -Add the Jitpack repository to your project's `settings.gradle` file: +```toml +# libs.versions.toml -```groovy -dependencyResolutionManagement { - repositories { - // ... - maven { url 'https://jitpack.io' } - } -} -``` +[versions] +pushpushgo-sdk-inapp = "4.0.0" -Then add the dependency in your app module's `build.gradle` file: +[libraries] +pushpushgo-sdk-inapp = { module = "com.pushpushgo:sdk-inapp", version.ref = "pushpushgo-sdk-inapp" } +``` -```groovy +```kotlin +// app/build.gradle.kts dependencies { - implementation 'com.github.ppgco.android-sdk:inappmessages:3.0.2' + implementation(libs.pushpushgo.sdk.inapp) } ``` ### Requirements -- Android API 23+ (Android 6.0+) +- Android API 26+ - Kotlin 1.6+ - Jetpack Compose (the library uses Compose for UI rendering) @@ -50,12 +47,24 @@ class MyApplication : Application() { override fun onCreate() { super.onCreate() - // Initialize the SDK - InAppMessagesSDK.initialize( - application = this, + // Initialize the SDK using configuration from AndroidManifest.xml + InAppMessages.initialize( + application = this, + // pushSubscriptionProvider = ..., required for push audiences/actions; see Action Handling + // customCodeHandler = ..., optional, see [Action Handling] section + ) + + // or + + // Initialize manually + InAppMessages.initialize( + application = this, + config = Config.create( projectId = "your-project-id", apiKey = "your-api-key", - debug = BuildConfig.DEBUG // Optional: enable diagnostic logging + ), + // pushSubscriptionProvider = ..., required for push audiences/actions; see Action Handling + // customCodeHandler = ..., optional, see [Action Handling] section ) } } @@ -73,13 +82,17 @@ Don't forget to add your application class to your AndroidManifest.xml: ## Basic Usage -### Displaying Automatic Messages +### Message Evaluation -Messages with the `APP_OPEN` trigger type will be displayed automatically when the app starts. +In-app messages are not displayed automatically. -### Displaying Messages on Screens/Routes +Messages are evaluated and displayed only after the application explicitly sets the current route. When a route is set, the SDK displays messages that: +- Are configured to display on all routes, or +- Have a route filter matching the provided route -To show messages configured for specific app screens: +### Displaying messages on the current route + +Call `showMessagesOnRoute` whenever the active screen or route changes. ```kotlin class MainActivity : AppCompatActivity() { @@ -91,7 +104,7 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() // Display messages for main screen, example: screen nav route name is "main_screen" - InAppMessagesSDK.getInstance().showActiveMessages("main_screen") + InAppMessages.getInstance().showMessagesOnRoute("main_screen") } } ``` @@ -179,16 +192,16 @@ Messages can be triggered based on custom events in your application: ```kotlin // Trigger messages after purchase completion fun onPurchaseCompleted(productId: String) { - InAppMessagesSDK.getInstance().showMessagesOnTrigger( + InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue( key = "purchase_completed", value = productId // value has to be a string - ) + )) } // Trigger review request after 3 feature uses fun checkAndShowReviewRequest(usageCount: Int) { if (usageCount >= 3) { - InAppMessagesSDK.getInstance().showMessagesOnTrigger("review_request") + InAppMessages.getInstance().showMessagesOnTrigger(Trigger.key("review_request")) } } ``` @@ -197,61 +210,67 @@ fun checkAndShowReviewRequest(usageCount: Int) { ### URL Redirections -By default, redirection actions open the URL in an external browser. No additional configuration is required. +By default, URL redirection actions open the target address in an external browser. +No additional configuration is required. + +--- + +### Push Subscription Actions + +In-app message buttons can be configured to **subscribe users to push notifications**. -### JavaScript Actions +The InAppMessages SDK does not handle push subscriptions itself. Provide a +`PushSubscriptionProvider` during initialization when messages use push-based +audience targeting or push subscription actions. Without it, the SDK cannot +reliably check the user's push subscription status or perform those actions. -To handle JavaScript actions from messages: +The PushPushGo PushNotifications SDK provides the default implementation. The +PushNotifications SDK must be initialized before requesting it: ```kotlin -InAppMessagesSDK.getInstance().setJsActionHandler { jsCode -> - // Process JavaScript code - when { - jsCode.contains("addToCart") -> { - // Handle add to cart - val productId = parseProductId(jsCode) - addToCart(productId) - } - jsCode.contains("applyDiscount") -> { - // Handle discount application - val discountCode = parseDiscountCode(jsCode) - applyDiscount(discountCode) - } - // Other JS actions - } -} +PushNotifications.initialize(application) + +InAppMessages.initialize( + application = application, + pushSubscriptionProvider = PushNotifications.getPushSubscriptionProvider(), +) ``` -## Advanced Features +A custom implementation may also be supplied. -### Resource Cleanup +--- -When logging out users or handling other state changes, you can clean up SDK resources: +### Custom Code Actions + +In-app message buttons can be configured to contain **custom code**. + +When such a button is clicked, the configured code is passed to a `CustomCodeHandler`, if supplied during SDK initialization. ```kotlin -fun onUserLogout() { - InAppMessagesSDK.getInstance().cleanup() - // Re-initialize SDK if needed after cleanup +interface CustomCodeHandler { + fun handle(code: String) } ``` -### Debugging +--- + + +## Debugging To facilitate debugging, enable debug mode during initialization: ```kotlin -InAppMessagesSDK.initialize( +InAppMessages.initialize( application = this, - projectId = "your-project-id", - apiKey = "your-api-key", - debug = true + config = Config.create( + projectId = "your-project-id", + apiKey = "your-api-key", + isDebug = true + ) ) ``` -Diagnostic messages will be visible in Logcat with these tags: -- `InAppMessageManager` -- `InAppUIController` -- `InAppMessageDisplayer` +Diagnostic messages will be visible in Logcat with the `[PushPushGo:InAppMessages]` tag. ## Trigger Examples @@ -260,37 +279,37 @@ Below are examples of typical message triggers that you can implement in your ap ### E-commerce ```kotlin // Abandoned cart -InAppMessagesSDK.getInstance().showMessagesOnTrigger("cart_abandoned") +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.key("cart_abandoned")) // Product added to cart -InAppMessagesSDK.getInstance().showMessagesOnTrigger("product_added", productId) +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue("product_added", productId)) // Order completed -InAppMessagesSDK.getInstance().showMessagesOnTrigger("order_completed", orderId) +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue("order_completed", orderId)) ``` ### Content Applications ```kotlin // Article read -InAppMessagesSDK.getInstance().showMessagesOnTrigger("article_read", articleId) +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue("article_read", articleId)) // Subscription expiring -InAppMessagesSDK.getInstance().showMessagesOnTrigger("subscription_expiring") +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.key("subscription_expiring")) // Free content limit reached -InAppMessagesSDK.getInstance().showMessagesOnTrigger("free_limit_reached") +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.key("free_limit_reached")) ``` ### Games ```kotlin // Level completed -InAppMessagesSDK.getInstance().showMessagesOnTrigger("level_completed", levelId) +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue("level_completed", levelId)) // Achievement unlocked -InAppMessagesSDK.getInstance().showMessagesOnTrigger("achievement_unlocked", achievementId) +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.keyValue("achievement_unlocked", achievementId)) // Game session ended -InAppMessagesSDK.getInstance().showMessagesOnTrigger("game_session_ended") +InAppMessages.getInstance().showMessagesOnTrigger(Trigger.key("game_session_ended")) ``` ## Troubleshooting diff --git a/inapp/api/inapp.api b/inapp/api/inapp.api new file mode 100644 index 00000000..fae63776 --- /dev/null +++ b/inapp/api/inapp.api @@ -0,0 +1,64 @@ +public final class com/pushpushgo/sdk/inapp/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public final class com/pushpushgo/sdk/inapp/InAppMessages { + public static final field $stable I + public static final field Companion Lcom/pushpushgo/sdk/inapp/InAppMessages$Companion; + public synthetic fun (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun getInstance ()Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun showMessagesOnRoute (Ljava/lang/String;)V + public final fun showMessagesOnTrigger (Lcom/pushpushgo/sdk/inapp/ui/Trigger;)V +} + +public final class com/pushpushgo/sdk/inapp/InAppMessages$Companion { + public final fun getInstance ()Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static synthetic fun initialize$default (Lcom/pushpushgo/sdk/inapp/InAppMessages$Companion;Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;ILjava/lang/Object;)Lcom/pushpushgo/sdk/inapp/InAppMessages; + public static synthetic fun initialize$default (Lcom/pushpushgo/sdk/inapp/InAppMessages$Companion;Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider;Lcom/pushpushgo/sdk/inapp/ui/CustomCodeHandler;ILjava/lang/Object;)Lcom/pushpushgo/sdk/inapp/InAppMessages; +} + +public abstract interface class com/pushpushgo/sdk/inapp/ui/CustomCodeHandler { + public abstract fun handle (Ljava/lang/String;)V +} + +public final class com/pushpushgo/sdk/inapp/ui/Trigger { + public static final field $stable I + public static final field Companion Lcom/pushpushgo/sdk/inapp/ui/Trigger$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; +} + +public final class com/pushpushgo/sdk/inapp/ui/Trigger$Companion { + public final fun key (Ljava/lang/String;)Lcom/pushpushgo/sdk/inapp/ui/Trigger; + public final fun keyValue (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/inapp/ui/Trigger; +} + +public final class com/pushpushgo/sdk/inapp/utils/InAppMessageHelper { + public static final field $stable I + public static final field INSTANCE Lcom/pushpushgo/sdk/inapp/utils/InAppMessageHelper; + public final fun ObserveNavBackStack (Landroidx/navigation/NavBackStackEntry;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public final fun ObserveNavigation (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V + public final fun createLifecycleObserver (Lkotlin/jvm/functions/Function0;)Landroidx/lifecycle/DefaultLifecycleObserver; + public final fun setupWithNavController (Landroidx/navigation/NavController;Lkotlin/jvm/functions/Function2;)Landroidx/navigation/NavController$OnDestinationChangedListener; + public static synthetic fun setupWithNavController$default (Lcom/pushpushgo/sdk/inapp/utils/InAppMessageHelper;Landroidx/navigation/NavController;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Landroidx/navigation/NavController$OnDestinationChangedListener; + public final fun showMessagesForFragment (Landroidx/fragment/app/Fragment;Ljava/lang/String;)V + public static synthetic fun showMessagesForFragment$default (Lcom/pushpushgo/sdk/inapp/utils/InAppMessageHelper;Landroidx/fragment/app/Fragment;Ljava/lang/String;ILjava/lang/Object;)V + public final fun showMessagesForScreen (Ljava/lang/String;)V +} + diff --git a/library-inappmessages/build.gradle.kts b/inapp/build.gradle.kts similarity index 51% rename from library-inappmessages/build.gradle.kts rename to inapp/build.gradle.kts index fe0d7588..af5291e4 100644 --- a/library-inappmessages/build.gradle.kts +++ b/inapp/build.gradle.kts @@ -1,56 +1,64 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.SourcesJar import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("com.google.devtools.ksp") version "2.2.0-2.0.2" - id("org.jetbrains.kotlin.plugin.serialization") version "2.2.0" - id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" - id("org.jlleitschuh.gradle.ktlint") version "13.0.0" - id("maven-publish") + alias(libs.plugins.android.library) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ktlint) + alias(libs.plugins.binary.validator) + alias(libs.plugins.maven.publish) } +group = "com.pushpushgo" +version = + requireNotNull(property("VERSION")) { + "VERSION property must be defined" + }.toString() + android { - namespace = "com.pushpushgo.inappmessages" + namespace = "com.pushpushgo.sdk.inapp" compileSdk = 36 defaultConfig { minSdk = 26 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } + + buildFeatures { + buildConfig = true + compose = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlin { compilerOptions { - jvmTarget = JvmTarget.fromTarget("17") - languageVersion = KotlinVersion.fromVersion("2.1") - apiVersion = KotlinVersion.fromVersion("2.1") - } - } - buildFeatures { - compose = true - } - - publishing { - singleVariant("release") { - withSourcesJar() + jvmTarget.set(JvmTarget.JVM_17) + languageVersion.set(KotlinVersion.KOTLIN_2_1) + apiVersion.set(KotlinVersion.KOTLIN_2_1) } } } dependencies { + api(project(":core")) + // Core & Appcompat implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) @@ -71,50 +79,47 @@ dependencies { implementation(libs.compose.material.icons) implementation(libs.compose.runtime) implementation(libs.compose.ui.text.fonts) // Google Fonts - implementation("androidx.emoji2:emoji2:1.6.0") - implementation("androidx.emoji2:emoji2-bundled:1.6.0") + implementation(libs.androidx.emoji2) + implementation(libs.androidx.emoji2.bundled) // Image Loading implementation(libs.coil.compose) // Serialization - implementation(libs.moshi.kotlin) ksp(libs.moshi.codegen) + implementation(libs.moshi.kotlin) implementation(libs.kotlinx.serialization) // Networking implementation(libs.retrofit) implementation(libs.retrofit.moshi) implementation(platform(libs.okhttp.bom)) - implementation("com.squareup.okhttp3:logging-interceptor") + implementation(libs.okhttp.logging) // Testing - testImplementation("junit:junit:4.13.2") - testImplementation("io.mockk:mockk:1.14.5") + testImplementation(libs.junit) + testImplementation(libs.mockk) testImplementation(libs.coroutines.test) - androidTestImplementation("androidx.test.ext:junit:1.3.0") - androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.espresso.core) } -tasks.register("androidSourcesJar") { - archiveClassifier.set("sources") - from( - android.sourceSets - .getByName("main") - .java.srcDirs, - ) +apiValidation { + ignoredProjects.add("sample") } -publishing { - publications { - create("release") { - groupId = "com.pushpushgo" - artifactId = "inappmessages" - version = libs.versions.sdk.get() +mavenPublishing { + coordinates(group.toString(), "sdk-inapp", version.toString()) - afterEvaluate { - from(components["release"]) - } - } + pom { + name.set("PushPushGo InAppMessages SDK") } + + configure( + AndroidSingleVariantLibrary( + javadocJar = JavadocJar.Empty(), + sourcesJar = SourcesJar.Sources(), + variant = "release", + ), + ) } diff --git a/inapp/gradle.properties b/inapp/gradle.properties new file mode 100644 index 00000000..275a949c --- /dev/null +++ b/inapp/gradle.properties @@ -0,0 +1 @@ +VERSION=4.0.0-SNAPSHOT diff --git a/inapp/sample/.gitignore b/inapp/sample/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/inapp/sample/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/inapp/sample/build.gradle.kts b/inapp/sample/build.gradle.kts new file mode 100644 index 00000000..3c078578 --- /dev/null +++ b/inapp/sample/build.gradle.kts @@ -0,0 +1,76 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.ktlint) +} + +android { + namespace = "com.pushpushgo.sdk.sample.inapp" + compileSdk = 36 + + buildFeatures { + buildConfig = true + } + + defaultConfig { + applicationId = "com.pushpushgo.sdk.sample.inapp" + minSdk = 28 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + manifestPlaceholders["PPG_PROJECT_ID"] = "" + manifestPlaceholders["PPG_API_KEY"] = "" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlin { + compilerOptions { + jvmTarget = JvmTarget.fromTarget("11") + } + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(project(":inapp")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.compose.activity) + + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + + implementation(libs.androidx.navigation.runtime) + implementation(libs.androidx.navigation.compose) + + testImplementation(libs.junit) + + androidTestImplementation(libs.androidx.test.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(platform(libs.compose.bom)) + androidTestImplementation(libs.compose.ui.test.junit4) + + debugImplementation(libs.compose.ui.tooling) + debugImplementation(libs.compose.ui.test.manifest) +} diff --git a/library-inappmessages/proguard-rules.pro b/inapp/sample/proguard-rules.pro similarity index 100% rename from library-inappmessages/proguard-rules.pro rename to inapp/sample/proguard-rules.pro diff --git a/sample-inapp/src/androidTest/java/com/pushpushgo/sample/ExampleInstrumentedTest.kt b/inapp/sample/src/androidTest/java/com/pushpushgo/sdk/sample/inapp/ExampleInstrumentedTest.kt similarity index 79% rename from sample-inapp/src/androidTest/java/com/pushpushgo/sample/ExampleInstrumentedTest.kt rename to inapp/sample/src/androidTest/java/com/pushpushgo/sdk/sample/inapp/ExampleInstrumentedTest.kt index 68533708..53056a3c 100644 --- a/sample-inapp/src/androidTest/java/com/pushpushgo/sample/ExampleInstrumentedTest.kt +++ b/inapp/sample/src/androidTest/java/com/pushpushgo/sdk/sample/inapp/ExampleInstrumentedTest.kt @@ -1,8 +1,8 @@ -package com.pushpushgo.sample +package com.pushpushgo.sdk.sample.inapp import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.* +import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -17,6 +17,6 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.pushpushgo.sample", appContext.packageName) + Assert.assertEquals("com.pushpushgo.sample", appContext.packageName) } } diff --git a/sample-inapp/src/main/AndroidManifest.xml b/inapp/sample/src/main/AndroidManifest.xml similarity index 76% rename from sample-inapp/src/main/AndroidManifest.xml rename to inapp/sample/src/main/AndroidManifest.xml index 2ea82488..7da81ef4 100644 --- a/sample-inapp/src/main/AndroidManifest.xml +++ b/inapp/sample/src/main/AndroidManifest.xml @@ -11,9 +11,8 @@ android:supportsRtl="true" android:theme="@style/Theme.Androidsdk"> @@ -21,6 +20,10 @@ + + + + diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/MainActivity.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/MainActivity.kt similarity index 65% rename from sample-inapp/src/main/java/com/pushpushgo/sample/MainActivity.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/MainActivity.kt index 3005c156..1c3e9d0c 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/MainActivity.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/MainActivity.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample +package com.pushpushgo.sdk.sample.inapp import android.os.Bundle import androidx.activity.ComponentActivity @@ -9,21 +9,15 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier -import com.pushpushgo.inappmessages.InAppMessagesSDK -import com.pushpushgo.sample.ui.MainNavHost -import com.pushpushgo.sample.ui.theme.AndroidsdkTheme +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.sample.inapp.ui.MainNavHost +import com.pushpushgo.sdk.sample.inapp.ui.theme.AndroidsdkTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - InAppMessagesSDK.initialize( - application = application, - projectId = BuildConfig.PPG_PROJECT_ID, - apiKey = BuildConfig.PPG_API_KEY, - debug = true, - baseUrl = "https://api.master1.qappg.co", - ) + InAppMessages.initialize(application) enableEdgeToEdge() setContent { diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/MainNavHost.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/MainNavHost.kt similarity index 71% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/MainNavHost.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/MainNavHost.kt index 4313bfe5..e30f0010 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/MainNavHost.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/MainNavHost.kt @@ -1,12 +1,12 @@ -package com.pushpushgo.sample.ui +package com.pushpushgo.sdk.sample.inapp.ui import androidx.compose.runtime.Composable import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.pushpushgo.sample.ui.screens.InfoScreen -import com.pushpushgo.sample.ui.screens.MainScreen -import com.pushpushgo.sample.ui.screens.OfferScreen +import com.pushpushgo.sdk.sample.inapp.ui.screens.InfoScreen +import com.pushpushgo.sdk.sample.inapp.ui.screens.MainScreen +import com.pushpushgo.sdk.sample.inapp.ui.screens.OfferScreen @Composable() internal fun MainNavHost() { diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/Screen.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/Screen.kt similarity index 78% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/Screen.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/Screen.kt index a7072ffb..5f2ff138 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/Screen.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/Screen.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui +package com.pushpushgo.sdk.sample.inapp.ui sealed class Screen( val route: String, diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/InfoScreen.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/InfoScreen.kt similarity index 84% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/InfoScreen.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/InfoScreen.kt index 61d62041..0a9730fd 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/InfoScreen.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/InfoScreen.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.screens +package com.pushpushgo.sdk.sample.inapp.ui.screens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,13 +13,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.pushpushgo.inappmessages.InAppMessagesSDK -import com.pushpushgo.sample.ui.Screen +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.sample.inapp.ui.Screen @Composable internal fun InfoScreen(navController: NavHostController) { LaunchedEffect(Screen.Info.route) { - InAppMessagesSDK.getInstance().showActiveMessages(Screen.Info.route) + InAppMessages.getInstance().showMessagesOnRoute(Screen.Info.route) } Column( diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/MainScreen.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/MainScreen.kt similarity index 87% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/MainScreen.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/MainScreen.kt index c690f705..d6d1f55b 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/MainScreen.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/MainScreen.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.screens +package com.pushpushgo.sdk.sample.inapp.ui.screens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,13 +13,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.pushpushgo.inappmessages.InAppMessagesSDK -import com.pushpushgo.sample.ui.Screen +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.sample.inapp.ui.Screen @Composable internal fun MainScreen(navController: NavHostController) { LaunchedEffect(Screen.Main.route) { - InAppMessagesSDK.getInstance().showActiveMessages(Screen.Main.route) + InAppMessages.getInstance().showMessagesOnRoute(Screen.Main.route) } Column( diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/OfferScreen.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/OfferScreen.kt similarity index 84% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/OfferScreen.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/OfferScreen.kt index fa1fc440..96081ee9 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/screens/OfferScreen.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/screens/OfferScreen.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.screens +package com.pushpushgo.sdk.sample.inapp.ui.screens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,13 +13,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.pushpushgo.inappmessages.InAppMessagesSDK -import com.pushpushgo.sample.ui.Screen +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.sample.inapp.ui.Screen @Composable internal fun OfferScreen(navController: NavHostController) { LaunchedEffect(Screen.Offer.route) { - InAppMessagesSDK.getInstance().showActiveMessages(Screen.Offer.route) + InAppMessages.getInstance().showMessagesOnRoute(Screen.Offer.route) } Column( diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Color.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Color.kt similarity index 83% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Color.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Color.kt index a6a1b985..3f8373cc 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Color.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.theme +package com.pushpushgo.sdk.sample.inapp.ui.theme import androidx.compose.ui.graphics.Color diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Theme.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Theme.kt similarity index 97% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Theme.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Theme.kt index 887ccd2f..62d3a128 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Theme.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.theme +package com.pushpushgo.sdk.sample.inapp.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme diff --git a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Type.kt b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Type.kt similarity index 95% rename from sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Type.kt rename to inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Type.kt index 4adf0636..65a97314 100644 --- a/sample-inapp/src/main/java/com/pushpushgo/sample/ui/theme/Type.kt +++ b/inapp/sample/src/main/java/com/pushpushgo/sdk/sample/inapp/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.sample.ui.theme +package com.pushpushgo.sdk.sample.inapp.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/sample-inapp/src/main/res/drawable/ic_launcher_background.xml b/inapp/sample/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from sample-inapp/src/main/res/drawable/ic_launcher_background.xml rename to inapp/sample/src/main/res/drawable/ic_launcher_background.xml diff --git a/sample-inapp/src/main/res/drawable/ic_launcher_foreground.xml b/inapp/sample/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from sample-inapp/src/main/res/drawable/ic_launcher_foreground.xml rename to inapp/sample/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/sample-inapp/src/main/res/mipmap-anydpi/ic_launcher.xml b/inapp/sample/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from sample-inapp/src/main/res/mipmap-anydpi/ic_launcher.xml rename to inapp/sample/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/sample-inapp/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/inapp/sample/src/main/res/mipmap-anydpi/ic_launcher_round.xml similarity index 100% rename from sample-inapp/src/main/res/mipmap-anydpi/ic_launcher_round.xml rename to inapp/sample/src/main/res/mipmap-anydpi/ic_launcher_round.xml diff --git a/sample-inapp/src/main/res/mipmap-hdpi/ic_launcher.webp b/inapp/sample/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-hdpi/ic_launcher.webp rename to inapp/sample/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/sample-inapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/inapp/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to inapp/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/sample-inapp/src/main/res/mipmap-mdpi/ic_launcher.webp b/inapp/sample/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-mdpi/ic_launcher.webp rename to inapp/sample/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/sample-inapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/inapp/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to inapp/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/sample-inapp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/inapp/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to inapp/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/sample-inapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/inapp/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to inapp/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/sample-inapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/inapp/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to inapp/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/sample-inapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/inapp/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to inapp/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/sample-inapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/inapp/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to inapp/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/sample-inapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/inapp/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from sample-inapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to inapp/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/sample-inapp/src/main/res/values/colors.xml b/inapp/sample/src/main/res/values/colors.xml similarity index 100% rename from sample-inapp/src/main/res/values/colors.xml rename to inapp/sample/src/main/res/values/colors.xml diff --git a/sample-inapp/src/main/res/values/strings.xml b/inapp/sample/src/main/res/values/strings.xml similarity index 100% rename from sample-inapp/src/main/res/values/strings.xml rename to inapp/sample/src/main/res/values/strings.xml diff --git a/sample-inapp/src/main/res/values/themes.xml b/inapp/sample/src/main/res/values/themes.xml similarity index 100% rename from sample-inapp/src/main/res/values/themes.xml rename to inapp/sample/src/main/res/values/themes.xml diff --git a/sample-inapp/src/test/java/com/pushpushgo/sample/ExampleUnitTest.kt b/inapp/sample/src/test/java/com/pushpushgo/sdk/sample/inapp/ExampleUnitTest.kt similarity index 72% rename from sample-inapp/src/test/java/com/pushpushgo/sample/ExampleUnitTest.kt rename to inapp/sample/src/test/java/com/pushpushgo/sdk/sample/inapp/ExampleUnitTest.kt index 23e49c38..44516c50 100644 --- a/sample-inapp/src/test/java/com/pushpushgo/sample/ExampleUnitTest.kt +++ b/inapp/sample/src/test/java/com/pushpushgo/sdk/sample/inapp/ExampleUnitTest.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.sample +package com.pushpushgo.sdk.sample.inapp -import org.junit.Assert.* +import org.junit.Assert import org.junit.Test /** @@ -11,6 +11,6 @@ import org.junit.Test class ExampleUnitTest { @Test fun addition_isCorrect() { - assertEquals(4, 2 + 2) + Assert.assertEquals(4, 2 + 2) } } diff --git a/library-inappmessages/src/main/AndroidManifest.xml b/inapp/src/main/AndroidManifest.xml similarity index 100% rename from library-inappmessages/src/main/AndroidManifest.xml rename to inapp/src/main/AndroidManifest.xml diff --git a/inapp/src/main/java/com/pushpushgo/sdk/inapp/InAppMessages.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/InAppMessages.kt new file mode 100644 index 00000000..279ff025 --- /dev/null +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/InAppMessages.kt @@ -0,0 +1,207 @@ +package com.pushpushgo.sdk.inapp + +import android.app.Application +import android.util.Log +import com.pushpushgo.sdk.core.api.Config +import com.pushpushgo.sdk.core.api.PushSubscriptionProvider +import com.pushpushgo.sdk.core.internal.ManifestConfigProvider +import com.pushpushgo.sdk.inapp.event.InAppMessageEvent +import com.pushpushgo.sdk.inapp.event.InAppMessageEventRepository +import com.pushpushgo.sdk.inapp.manager.InAppMessageManager +import com.pushpushgo.sdk.inapp.manager.InAppMessageManagerImpl +import com.pushpushgo.sdk.inapp.network.InAppEventApi +import com.pushpushgo.sdk.inapp.network.InAppListGetApi +import com.pushpushgo.sdk.inapp.network.RetrofitProvider +import com.pushpushgo.sdk.inapp.persistence.InAppMessagePersistenceImpl +import com.pushpushgo.sdk.inapp.repository.InAppMessageRepositoryImpl +import com.pushpushgo.sdk.inapp.ui.CustomCodeHandler +import com.pushpushgo.sdk.inapp.ui.InAppMessageDisplayer +import com.pushpushgo.sdk.inapp.ui.InAppMessageDisplayerImpl +import com.pushpushgo.sdk.inapp.ui.InAppUIController +import com.pushpushgo.sdk.inapp.ui.Trigger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import retrofit2.Retrofit + +class InAppMessages private constructor( + private val application: Application, + private val config: Config, + private val pushSubscriptionProvider: PushSubscriptionProvider? = null, + private val customCodeHandler: CustomCodeHandler? = null, +) { + private val retrofit: Retrofit by lazy { + RetrofitProvider.buildRetrofit(config.apiUrl) + } + private val api: InAppListGetApi by lazy { + retrofit.create(InAppListGetApi::class.java) + } + private val eventApi: InAppEventApi by lazy { + retrofit.create(InAppEventApi::class.java) + } + private val eventRepository by lazy { + InAppMessageEventRepository(eventApi, debug = config.isDebug) + } + + internal suspend fun dispatchInAppEvent( + action: String, + inAppId: String, + ) { + try { + eventRepository.sendEvent( + projectId = config.projectId, + token = config.apiKey, + event = InAppMessageEvent(action = action, inApp = inAppId), + ) + } catch (e: Exception) { + if (config.isDebug) { + Log.e(TAG, "Failed to send in-app event", e) + } + } + } + + private val sdkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val manager: InAppMessageManager + private val displayer: InAppMessageDisplayer + private val uiController: InAppUIController + + companion object { + internal const val TAG = "[PushPushGo:InAppMessages]" + + @Volatile + private var INSTANCE: InAppMessages? = null + + /** + * Initializes the InAppMessages SDK using configuration defined in AndroidManifest.xml. + * + * Subsequent calls return the same instance. + * + * @throws IllegalStateException if required manifest values are missing. + */ + @JvmStatic + @JvmOverloads + fun initialize( + application: Application, + pushSubscriptionProvider: PushSubscriptionProvider? = null, + customCodeHandler: CustomCodeHandler? = null, + ): InAppMessages = + INSTANCE ?: synchronized(this) { + INSTANCE ?: InAppMessages( + application = application, + config = ManifestConfigProvider(application).provide(), + pushSubscriptionProvider = pushSubscriptionProvider, + customCodeHandler = customCodeHandler, + ).also { INSTANCE = it } + } + + /** + * Initializes the InAppMessages SDK using an explicit [Config]. + * + * Subsequent calls return the same instance. + */ + @JvmStatic + @JvmOverloads + fun initialize( + application: Application, + config: Config, + pushSubscriptionProvider: PushSubscriptionProvider? = null, + customCodeHandler: CustomCodeHandler? = null, + ): InAppMessages = + INSTANCE ?: synchronized(this) { + INSTANCE ?: InAppMessages( + application = application, + config = config, + pushSubscriptionProvider = pushSubscriptionProvider, + customCodeHandler = customCodeHandler, + ).also { + INSTANCE = it + } + } + + @JvmStatic + fun getInstance(): InAppMessages = INSTANCE ?: throw IllegalStateException("InAppMessages SDK is not initialized!") + } + + init { + val persistence = InAppMessagePersistenceImpl(application, config.isDebug) + val repository = InAppMessageRepositoryImpl(api, config.projectId, config.apiKey, persistence, config.isDebug) + manager = + InAppMessageManagerImpl( + scope = sdkScope, + repository = repository, + persistence = persistence, + context = application, + debug = config.isDebug, + pushSubscriptionProvider = pushSubscriptionProvider, + ) + displayer = + InAppMessageDisplayerImpl( + persistence = persistence, + debug = config.isDebug, + onMessageDismissed = { + sdkScope.launch { + manager.refreshActiveMessages(manager.getRoute()) + } + }, + onMessageEvent = { eventType, message, ctaIndex -> + sdkScope.launch { + when (eventType) { + "show" -> dispatchInAppEvent("inapp.show", message.id) + "close" -> dispatchInAppEvent("inapp.close", message.id) + "cta" -> dispatchInAppEvent("inapp.cta.$ctaIndex", message.id) + } + } + }, + pushSubscriptionProvider = pushSubscriptionProvider, + customCodeHandler = customCodeHandler, + ) + uiController = InAppUIController(application, manager, displayer, config.isDebug) + + sdkScope.launch { + manager.initialize() + } + uiController.start() + } + + /** + * Displays in-app messages applicable to the given route. + * + * Messages will be shown if they: + * - Are configured to display on all pages + * - Have a route filter that matches the provided route + * + * This method should be called: + * - Once when the app starts + * - Whenever the active route changes + * + * @param route Non-blank route identifier. + * + * @throws IllegalArgumentException if route is blank. + */ + fun showMessagesOnRoute(route: String) { + require(route.isNotBlank()) { + "Route name must not me blank" + } + + sdkScope.launch { + manager.refreshActiveMessages(route) + } + } + + /** + * Displays in-app messages associated with a custom trigger. + * + * Only messages whose trigger conditions match the provided trigger + * will be displayed. + */ + fun showMessagesOnTrigger(trigger: Trigger) { + sdkScope.launch { + val messageToShow = manager.trigger(trigger.key, trigger.value) + + if (messageToShow != null) { + uiController.displayCustomMessage(messageToShow) + } + } + } +} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEvent.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEvent.kt similarity index 76% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEvent.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEvent.kt index 4dcd5442..de54f116 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEvent.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEvent.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.data.event +package com.pushpushgo.sdk.inapp.event import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEventRepository.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEventRepository.kt similarity index 72% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEventRepository.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEventRepository.kt index 60380dc2..b22ceb8c 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/data/event/InAppMessageEventRepository.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/event/InAppMessageEventRepository.kt @@ -1,7 +1,8 @@ -package com.pushpushgo.inappmessages.data.event +package com.pushpushgo.sdk.inapp.event import android.util.Log -import com.pushpushgo.inappmessages.network.InAppEventApi +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.network.InAppEventApi import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -20,8 +21,9 @@ internal class InAppMessageEventRepository( val response = api.sendInAppEvent(token, projectId, event) if (!response.isSuccessful) { if (debug) { - Log.e("InAppEventRepo", "Failed to send event: ${response.code()} ${response.message()}") + Log.e(InAppMessages.TAG, "[EventRepository] Failed to send event: ${response.code()} ${response.message()}") } + throw HttpException(response) } } diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManager.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManager.kt similarity index 67% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManager.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManager.kt index e6dd7038..9b31a092 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManager.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManager.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.inappmessages.manager +package com.pushpushgo.sdk.inapp.manager -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage import kotlinx.coroutines.flow.Flow internal interface InAppMessageManager { @@ -15,7 +15,9 @@ internal interface InAppMessageManager { fun getActiveMessages(): List - suspend fun refreshActiveMessages(route: String? = null) + fun getRoute(): String? + + suspend fun refreshActiveMessages(route: String?) suspend fun isMessageEligible(message: InAppMessage): Boolean } diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManagerImpl.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManagerImpl.kt similarity index 64% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManagerImpl.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManagerImpl.kt index 0b30793c..a2e7f474 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/manager/InAppMessageManagerImpl.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/manager/InAppMessageManagerImpl.kt @@ -1,18 +1,22 @@ -package com.pushpushgo.inappmessages.manager +package com.pushpushgo.sdk.inapp.manager import android.content.Context import android.util.Log -import com.pushpushgo.inappmessages.model.DeviceType -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.OSType -import com.pushpushgo.inappmessages.model.PlatformType -import com.pushpushgo.inappmessages.model.ShowAgainType -import com.pushpushgo.inappmessages.model.TriggerType -import com.pushpushgo.inappmessages.persistence.InAppMessagePersistence -import com.pushpushgo.inappmessages.repository.InAppMessageRepository -import com.pushpushgo.inappmessages.utils.DeviceInfoProvider -import com.pushpushgo.inappmessages.utils.PushNotificationStatusProvider +import com.pushpushgo.sdk.core.api.PushSubscriptionProvider +import com.pushpushgo.sdk.core.internal.NotificationPermissionProvider +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.model.DeviceType +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.OSType +import com.pushpushgo.sdk.inapp.model.PlatformType +import com.pushpushgo.sdk.inapp.model.ShowAgainType +import com.pushpushgo.sdk.inapp.model.TriggerType +import com.pushpushgo.sdk.inapp.model.UserAudienceType +import com.pushpushgo.sdk.inapp.persistence.InAppMessagePersistence +import com.pushpushgo.sdk.inapp.repository.InAppMessageRepository +import com.pushpushgo.sdk.inapp.utils.DeviceInfoProvider import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -33,12 +37,8 @@ internal class InAppMessageManagerImpl( private val persistence: InAppMessagePersistence, private val context: Context, private val debug: Boolean = false, + private val pushSubscriptionProvider: PushSubscriptionProvider? = null, ) : InAppMessageManager { - // Provider for accessing push notification subscription status - private val notificationStatusProvider = PushNotificationStatusProvider(context) - - private val tag = "InAppMessageManager" - // Schedule refresh configuration private var scheduleRefreshJob: Job? = null private val scheduleRefreshInterval = 60_000L // Check schedules every minute @@ -58,6 +58,8 @@ internal class InAppMessageManagerImpl( private val messagesUpdateMutex = Mutex() private val refreshJobMutex = Mutex() + private val hasInitialized = CompletableDeferred() + // Device info private val currentDeviceType by lazy { DeviceInfoProvider.getCurrentDeviceType(context) } private val currentOsType = DeviceInfoProvider.getCurrentOSType() @@ -65,25 +67,23 @@ internal class InAppMessageManagerImpl( override suspend fun initialize() { try { if (debug) { - Log.d(tag, "Initializing InAppMessageManager...") + Log.d(InAppMessages.TAG, "[Manager] Initializing...") } // Fetch messages from API (with cache support) refreshMessagesFromApi() - // Build trigger map and refresh active messages - buildTriggerMap(allMessages) - refreshActiveMessages() - // Start periodic schedule checks startScheduleRefresh() + hasInitialized.complete(Unit) + if (debug) { - Log.d(tag, "InAppMessageManager initialized successfully") + Log.d(InAppMessages.TAG, "[Manager] initialized successfully") } } catch (e: Exception) { if (e is CancellationException) throw e - Log.e(tag, "Error initializing InAppMessageManager: ${e.message}", e) + Log.e(InAppMessages.TAG, "[Manager] Error initializing: ${e.message}", e) } } @@ -99,7 +99,7 @@ internal class InAppMessageManagerImpl( } if (debug) { - Log.d(tag, "Fetched ${messages.size} messages from API") + Log.d(InAppMessages.TAG, "[Manager] Fetched ${messages.size} messages from API") } // Update collections @@ -110,7 +110,7 @@ internal class InAppMessageManagerImpl( buildTriggerMap(messages) } catch (e: Exception) { if (e is CancellationException) throw e - Log.e(tag, "Error refreshing messages from API: ${e.message}", e) + Log.e(InAppMessages.TAG, "[Manager] Error refreshing messages from API: ${e.message}", e) } } @@ -125,7 +125,7 @@ internal class InAppMessageManagerImpl( // Start a new job in the current scope to leverage its lifecycle scheduleRefreshJob = - scope.launch(Dispatchers.IO) { + scope.launch { try { while (true) { delay(scheduleRefreshInterval) @@ -133,7 +133,7 @@ internal class InAppMessageManagerImpl( // Don't block on refresh, just log and continue if there's an error try { if (debug) { - Log.d(tag, "Periodic refresh: fetching messages from API...") + Log.d(InAppMessages.TAG, "[Manager] Periodic refresh: fetching messages from API...") } // Refresh messages from API (with cache support via If-None-Match) @@ -142,11 +142,11 @@ internal class InAppMessageManagerImpl( // Then refresh active messages based on updated data refreshActiveMessages(currentRoute) } catch (e: Exception) { - Log.e(tag, "Error during periodic schedule refresh: ${e.message}") + Log.e(InAppMessages.TAG, "[Manager] Error during periodic schedule refresh: ${e.message}") } } } catch (e: Exception) { - Log.e(tag, "Schedule refresh job failed: ${e.message}") + Log.e(InAppMessages.TAG, "[Manager] Schedule refresh job failed: ${e.message}") } } } @@ -184,7 +184,7 @@ internal class InAppMessageManagerImpl( // 0. Check if message is enabled if (!message.enabled) { if (debug) { - Log.d(tag, "Message [${message.id}] is disabled - not eligible") + Log.d(InAppMessages.TAG, "[Manager] Message [${message.id}] is disabled - not eligible") } return@withContext false } @@ -196,7 +196,7 @@ internal class InAppMessageManagerImpl( ) ) { if (debug) { - Log.d(tag, "Message [${message.id}] permanently dismissed - not eligible") + Log.d(InAppMessages.TAG, "[Manager] Message [${message.id}] permanently dismissed - not eligible") } return@withContext false } @@ -204,17 +204,17 @@ internal class InAppMessageManagerImpl( // 2. Check schedule window (absolute check) if (!isInScheduleWindow(message)) { if (debug) { - Log.d(tag, "Message [${message.id}] outside schedule window - not eligible") + Log.d(InAppMessages.TAG, "[Manager] Message [${message.id}] outside schedule window - not eligible") } return@withContext false } // 3. Check user audience type - if (!notificationStatusProvider.matchesAudienceType(message.audience.userType)) { + if (!userMatchesAudienceType(message.audience.userType)) { if (debug) { Log.d( - tag, - "Message [${message.id}] audience mismatch (${message.audience.userType}) - not eligible", + InAppMessages.TAG, + "[Manager] Message [${message.id}] audience mismatch (${message.audience.userType}) - not eligible", ) } return@withContext false @@ -236,8 +236,8 @@ internal class InAppMessageManagerImpl( if (elapsedSinceLastDismissal < requiredCooldownMs) { if (debug) { Log.d( - tag, - "Message [${message.id}] in cooldown: ${elapsedSinceLastDismissal}ms/${requiredCooldownMs}ms", + InAppMessages.TAG, + "[Manager] Message [${message.id}] in cooldown: ${elapsedSinceLastDismissal}ms/${requiredCooldownMs}ms", ) } return@withContext false // Still in cooldown since last dismissal @@ -248,60 +248,49 @@ internal class InAppMessageManagerImpl( return@withContext true } - /** - * Refresh the list of active messages based on current conditions - * Filters messages by eligibility, schedule, device type, OS type, and expiration - */ override suspend fun refreshActiveMessages(route: String?) { - // If a new route is explicitly provided (on navigation), update the manager's internal state. - if (route != null) { - this.currentRoute = route - } + hasInitialized.await() - // The route to use for this specific refresh operation is the one passed in, - // or the one we have stored if the call is for a generic refresh (e.g., on dismissal). - val effectiveRoute = route ?: this.currentRoute + // If a new route is explicitly provided (on navigation), update the manager's internal state. + this.currentRoute = route val newJob = - scope.launch(Dispatchers.IO) { + scope.launch { try { if (debug) { - Log.d(tag, "Refreshing active messages for route: ${effectiveRoute ?: "ENTER"}") + Log.d(InAppMessages.TAG, "[Manager] Refreshing active messages for route: $route") } val eventBasedMessages = - allMessages.filter { msg -> - // CUSTOM_TRIGGER triggers are handled by the `trigger` method, not by general refresh. - if (msg.settings.triggerType == TriggerType.CUSTOM_TRIGGER) { - return@filter false - } + if (route != null) { + allMessages.filter { msg -> + // CUSTOM_TRIGGER triggers are handled by the `trigger` method, not by general refresh. + if (msg.settings.triggerType == TriggerType.CUSTOM_TRIGGER) { + return@filter false + } - val displayOnRules = msg.settings.displayOn + val displayOnRules = msg.settings.displayOn - if (displayOnRules.isEmpty()) { - return@filter true - } + if (displayOnRules.isEmpty()) { + return@filter true + } - // Specific route rules exist. The message should only appear on these routes. + val (displayed, hidden) = displayOnRules.partition { it.display } + val isDisplayed = displayed.any { it.path == route } + val isHidden = hidden.any { it.path == route } - if (effectiveRoute == null) { - // If we're not on a specific route, don't show route-specific messages. - return@filter false - } + if (displayed.isEmpty() && !isHidden) { + return@filter true + } - val (displayed, hidden) = displayOnRules.partition { it.display } - val isDisplayed = displayed.any { it.path == effectiveRoute } - val isHidden = hidden.any { it.path == effectiveRoute } + if (isDisplayed && !isHidden) { + return@filter true + } - if (displayed.isEmpty() && !isHidden) { - return@filter true + false } - - if (isDisplayed && !isHidden) { - return@filter true - } - - false + } else { + emptyList() } val initiallyFiltered = @@ -336,7 +325,9 @@ internal class InAppMessageManagerImpl( finalEligibleMessages.sortedWith( compareBy { message -> when (val priority = message.settings.priority) { - 0 -> Int.MAX_VALUE // Lowest priority (0 = displayed last) + 0 -> Int.MAX_VALUE + + // Lowest priority (0 = displayed last) else -> priority // 1 = highest, 2 = second, etc. } }, @@ -347,11 +338,11 @@ internal class InAppMessageManagerImpl( _messagesFlow.value = activeMessages.toList() if (debug) { - Log.d(tag, "Active messages refreshed: ${newActiveMessages.size} eligible messages") + Log.d(InAppMessages.TAG, "[Manager] Active messages refreshed: ${newActiveMessages.size} eligible messages") } } } catch (e: Exception) { - Log.e(tag, "Error refreshing active messages for route: ${effectiveRoute ?: "ENTER"}", e) + Log.e(InAppMessages.TAG, "[Manager] Error refreshing active messages for route: $route", e) } } @@ -376,7 +367,7 @@ internal class InAppMessageManagerImpl( jobToWaitFor?.join() if (debug) { - Log.d(tag, "Triggering custom message: key='$key', value='$value'") + Log.d(InAppMessages.TAG, "[Manager] Triggering custom message: key='$key', value='$value'") } val potentialMessages = @@ -391,7 +382,7 @@ internal class InAppMessageManagerImpl( if (potentialMessages.isEmpty()) { if (debug) { - Log.d(tag, "No messages found for trigger key='$key', value='$value'") + Log.d(InAppMessages.TAG, "[Manager] No messages found for trigger key='$key', value='$value'") } return null } @@ -399,7 +390,9 @@ internal class InAppMessageManagerImpl( for (msg in potentialMessages.sortedWith( compareBy { message -> when (val priority = message.settings.priority) { - 0 -> Int.MAX_VALUE // Lowest priority (0 = displayed last) + 0 -> Int.MAX_VALUE + + // Lowest priority (0 = displayed last) else -> priority // 1 = highest, 2 = second, etc. } }, @@ -410,7 +403,7 @@ internal class InAppMessageManagerImpl( persistence.setFirstEligibleAt(msg.id, System.currentTimeMillis()) } if (debug) { - Log.d(tag, "Found eligible message [${msg.id}] for trigger '$key'") + Log.d(InAppMessages.TAG, "[Manager] Found eligible message [${msg.id}] for trigger '$key'") } return msg } @@ -419,29 +412,41 @@ internal class InAppMessageManagerImpl( return null } - private suspend fun isInScheduleWindow(msg: InAppMessage): Boolean = - withContext(Dispatchers.Default) { - val schedule = msg.schedule ?: return@withContext true // No schedule means always in window + private fun isInScheduleWindow(msg: InAppMessage): Boolean { + val schedule = msg.schedule ?: return true // No schedule means always in window - // Get current time in system default zone - val currentTime = ZonedDateTime.now() + // Get current time in system default zone + val currentTime = ZonedDateTime.now() - // If there's no schedule constraints, message is always in window - if (schedule.startTime == null && schedule.endTime == null) { - return@withContext true - } + // If there's no schedule constraints, message is always in window + if (schedule.startTime == null && schedule.endTime == null) { + return true + } + + // Normalize time zones for accurate comparison + val normalizedStartTime = schedule.startTime?.withZoneSameInstant(currentTime.zone) + val normalizedEndTime = schedule.endTime?.withZoneSameInstant(currentTime.zone) + + // Check if current time is within schedule bounds + val afterStart = normalizedStartTime == null || !currentTime.isBefore(normalizedStartTime) + val beforeEnd = normalizedEndTime == null || currentTime.isBefore(normalizedEndTime) + val isInWindow = afterStart && beforeEnd - // Normalize time zones for accurate comparison - val normalizedStartTime = schedule.startTime?.withZoneSameInstant(currentTime.zone) - val normalizedEndTime = schedule.endTime?.withZoneSameInstant(currentTime.zone) + return isInWindow + } - // Check if current time is within schedule bounds - val afterStart = normalizedStartTime == null || !currentTime.isBefore(normalizedStartTime) - val beforeEnd = normalizedEndTime == null || currentTime.isBefore(normalizedEndTime) - val isInWindow = afterStart && beforeEnd + private fun userMatchesAudienceType(audienceType: UserAudienceType): Boolean { + val isSubscribed = pushSubscriptionProvider?.isSubscribed() ?: false + val canPostNotifications = NotificationPermissionProvider.canPostNotifications(context) + val isNotificationChannelEnabled = pushSubscriptionProvider?.isNotificationChannelEnabled() ?: false - return@withContext isInWindow + return when (audienceType) { + UserAudienceType.ALL -> true + UserAudienceType.SUBSCRIBER -> isSubscribed && canPostNotifications && isNotificationChannelEnabled + UserAudienceType.NON_SUBSCRIBER -> !isSubscribed || !canPostNotifications || !isNotificationChannelEnabled + UserAudienceType.NOTIFICATIONS_BLOCKED -> !canPostNotifications || !isNotificationChannelEnabled } + } /** * Get the current list of active messages @@ -451,4 +456,6 @@ internal class InAppMessageManagerImpl( * @return List of active messages that are eligible to be shown */ override fun getActiveMessages(): List = activeMessages.toList() + + override fun getRoute(): String? = currentRoute } diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Alignment.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Alignment.kt similarity index 86% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Alignment.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Alignment.kt index 894b722e..d648a5b8 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Alignment.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Alignment.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/AnimationType.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/AnimationType.kt similarity index 83% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/AnimationType.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/AnimationType.kt index aef382d5..3c6e8009 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/AnimationType.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/AnimationType.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayOn.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayOn.kt similarity index 88% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayOn.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayOn.kt index cb59a2fe..890b83e2 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayOn.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayOn.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayType.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayType.kt similarity index 83% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayType.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayType.kt index 7d11aa4c..19755a00 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/DisplayType.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/DisplayType.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontFamily.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontFamily.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontFamily.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontFamily.kt index 67e4b45e..71e6aad7 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontFamily.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontFamily.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontStyle.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontStyle.kt similarity index 86% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontStyle.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontStyle.kt index fccab4e2..7a0d062e 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/FontStyle.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/FontStyle.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppActionType.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppActionType.kt similarity index 87% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppActionType.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppActionType.kt index b2b6d1c4..70510e45 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppActionType.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppActionType.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessage.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessage.kt similarity index 98% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessage.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessage.kt index a0cfbd68..e1cffeda 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessage.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessage.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAction.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAction.kt similarity index 95% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAction.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAction.kt index 1b49e69b..fce7938f 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAction.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAction.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAudience.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAudience.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAudience.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAudience.kt index c828f9e5..7f340dbe 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageAudience.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageAudience.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageDescription.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageDescription.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageDescription.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageDescription.kt index 39b989d9..fed23c0d 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageDescription.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageDescription.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageImage.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageImage.kt similarity index 88% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageImage.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageImage.kt index dd32f019..4c36ef8f 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageImage.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageImage.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageLayout.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageLayout.kt similarity index 94% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageLayout.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageLayout.kt index c9a82ded..1a4d083a 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageLayout.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageLayout.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageSettings.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageSettings.kt similarity index 95% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageSettings.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageSettings.kt index cab9a1f5..56c325bb 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageSettings.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageSettings.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageStyle.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageStyle.kt similarity index 95% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageStyle.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageStyle.kt index c4348c21..204ee4c6 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageStyle.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageStyle.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageTitle.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageTitle.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageTitle.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageTitle.kt index 53375d42..2fe920f0 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/InAppMessageTitle.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/InAppMessageTitle.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Placement.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Placement.kt similarity index 91% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Placement.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Placement.kt index 55a33fb9..b54adb6c 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/Placement.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/Placement.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/ShowAgainType.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/ShowAgainType.kt similarity index 84% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/ShowAgainType.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/ShowAgainType.kt index 703f8532..81bb7b9c 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/ShowAgainType.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/ShowAgainType.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/TargetType.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/TargetType.kt similarity index 82% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/TargetType.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/TargetType.kt index 1c980e96..94756c3a 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/TargetType.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/TargetType.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.model +package com.pushpushgo.sdk.inapp.model import com.squareup.moshi.Json diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/network/InAppMessagesResponse.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/network/InAppMessagesResponse.kt similarity index 77% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/network/InAppMessagesResponse.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/model/network/InAppMessagesResponse.kt index c8e8af35..fe5da43b 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/model/network/InAppMessagesResponse.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/model/network/InAppMessagesResponse.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.inappmessages.model.network +package com.pushpushgo.sdk.inapp.model.network -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/InAppApi.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/network/InAppApi.kt similarity index 83% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/InAppApi.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/network/InAppApi.kt index d575f918..0ca1d14a 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/InAppApi.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/network/InAppApi.kt @@ -1,7 +1,7 @@ -package com.pushpushgo.inappmessages.network +package com.pushpushgo.sdk.inapp.network -import com.pushpushgo.inappmessages.data.event.InAppMessageEvent -import com.pushpushgo.inappmessages.model.network.InAppMessagesResponse +import com.pushpushgo.sdk.inapp.event.InAppMessageEvent +import com.pushpushgo.sdk.inapp.model.network.InAppMessagesResponse import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/RetrofitProvider.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/network/RetrofitProvider.kt similarity index 84% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/RetrofitProvider.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/network/RetrofitProvider.kt index 5928f9c5..06876953 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/network/RetrofitProvider.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/network/RetrofitProvider.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.inappmessages.network +package com.pushpushgo.sdk.inapp.network -import com.pushpushgo.inappmessages.utils.ZonedDateTimeAdapter +import com.pushpushgo.sdk.inapp.utils.ZonedDateTimeAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.OkHttpClient @@ -22,7 +22,7 @@ internal object RetrofitProvider { val moshi = Moshi .Builder() - .add(ZonedDateTimeAdapter.FACTORY) + .add(ZonedDateTimeAdapter.Companion.FACTORY) .add(KotlinJsonAdapterFactory()) .build() diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistence.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistence.kt similarity index 88% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistence.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistence.kt index 2554fdd1..82b02d40 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistence.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistence.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.inappmessages.persistence +package com.pushpushgo.sdk.inapp.persistence -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage internal interface InAppMessagePersistence { fun isMessageDismissed(messageId: String): Boolean diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistenceImpl.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistenceImpl.kt similarity index 81% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistenceImpl.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistenceImpl.kt index 0a00c27c..c9602d30 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/persistence/InAppMessagePersistenceImpl.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/persistence/InAppMessagePersistenceImpl.kt @@ -1,11 +1,12 @@ -package com.pushpushgo.inappmessages.persistence +package com.pushpushgo.sdk.inapp.persistence import android.content.Context import android.content.SharedPreferences import android.util.Log import androidx.core.content.edit -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.utils.ZonedDateTimeAdapter +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.utils.ZonedDateTimeAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types @@ -17,7 +18,7 @@ internal class InAppMessagePersistenceImpl( private val moshi: Moshi = Moshi .Builder() - .add(ZonedDateTimeAdapter.FACTORY) + .add(ZonedDateTimeAdapter.Companion.FACTORY) .addLast(KotlinJsonAdapterFactory()) .build(), ) : InAppMessagePersistence { @@ -28,7 +29,6 @@ internal class InAppMessagePersistenceImpl( private val messagesAdapter: JsonAdapter> = moshi.adapter(listType) companion object { - private const val TAG = "InAppMsgPersistence" private const val KEY_ETAG = "etag" private const val KEY_CACHED_MESSAGES = "cached_messages" private const val KEY_CACHE_TIMESTAMP = "cache_timestamp" @@ -41,7 +41,7 @@ internal class InAppMessagePersistenceImpl( override fun markMessageDismissed(messageId: String) { if (debug) { - Log.d(TAG, "Marking message [$messageId] as dismissed") + Log.d(InAppMessages.TAG, "[Persistence] Marking message [$messageId] as dismissed") } prefs.edit { putBoolean("dismissed_$messageId", true) } setLastDismissedAt(messageId, System.currentTimeMillis()) @@ -87,14 +87,14 @@ internal class InAppMessagePersistenceImpl( return if (isExpired) { // Cache expired - clear and return null to force fresh fetch if (debug) { - Log.d(TAG, "Cache expired, clearing and forcing fresh fetch") + Log.d(InAppMessages.TAG, "[Persistence] Cache expired, clearing and forcing fresh fetch") } clearCache() null } else { val etag = prefs.getString(KEY_ETAG, null) if (debug) { - Log.d(TAG, "Retrieved stored ETag: ${etag ?: "none"}") + Log.d(InAppMessages.TAG, "[Persistence] Retrieved stored ETag: ${etag ?: "none"}") } etag } @@ -106,7 +106,7 @@ internal class InAppMessagePersistenceImpl( ) { val messagesJson = messagesAdapter.toJson(messages) if (debug) { - Log.d(TAG, "Saving cache: ETag=$etag, ${messages.size} messages") + Log.d(InAppMessages.TAG, "[Persistence] Saving cache: ETag=$etag, ${messages.size} messages") } prefs.edit { @@ -122,13 +122,13 @@ internal class InAppMessagePersistenceImpl( return try { val messages = messagesAdapter.fromJson(messagesJson) ?: emptyList() if (debug) { - Log.d(TAG, "Retrieved ${messages.size} cached messages") + Log.d(InAppMessages.TAG, "[Persistence] Retrieved ${messages.size} cached messages") } messages } catch (_: Exception) { // JSON parsing failed - clear cache and return null if (debug) { - Log.d(TAG, "Failed to parse cached messages, clearing cache") + Log.d(InAppMessages.TAG, "[Persistence] Failed to parse cached messages, clearing cache") } clearCache() null @@ -137,7 +137,7 @@ internal class InAppMessagePersistenceImpl( override fun clearCache() { if (debug) { - Log.d(TAG, "Clearing cache") + Log.d(InAppMessages.TAG, "[Persistence] Clearing cache") } prefs.edit { remove(KEY_ETAG) diff --git a/inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepository.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepository.kt new file mode 100644 index 00000000..aed72aec --- /dev/null +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepository.kt @@ -0,0 +1,7 @@ +package com.pushpushgo.sdk.inapp.repository + +import com.pushpushgo.sdk.inapp.model.InAppMessage + +internal interface InAppMessageRepository { + suspend fun fetchMessages(): List +} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepositoryImpl.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepositoryImpl.kt similarity index 65% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepositoryImpl.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepositoryImpl.kt index 104ce39f..889e97bc 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepositoryImpl.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/repository/InAppMessageRepositoryImpl.kt @@ -1,9 +1,10 @@ -package com.pushpushgo.inappmessages.repository +package com.pushpushgo.sdk.inapp.repository import android.util.Log -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.network.InAppListGetApi -import com.pushpushgo.inappmessages.persistence.InAppMessagePersistence +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.network.InAppListGetApi +import com.pushpushgo.sdk.inapp.persistence.InAppMessagePersistence import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -14,10 +15,6 @@ internal class InAppMessageRepositoryImpl( private val persistence: InAppMessagePersistence, private val debug: Boolean = false, ) : InAppMessageRepository { - companion object { - private const val TAG = "InAppMsgRepo" - } - override suspend fun fetchMessages(): List = try { withContext(Dispatchers.IO) { @@ -35,18 +32,18 @@ internal class InAppMessageRepositoryImpl( // Fresh data received val messages = response.body()?.data ?: emptyList() if (debug) { - Log.d(TAG, "Received fresh data: ${messages.size} messages") + Log.d(InAppMessages.TAG, "[Repository] Received fresh data: ${messages.size} messages") } // Save ETag and cache the payload val newETag = response.headers()["ETag"] if (newETag != null) { if (debug) { - Log.d(TAG, "Saving cache with ETag: $newETag") + Log.d(InAppMessages.TAG, "[Repository] Saving cache with ETag: $newETag") } persistence.saveCache(newETag, messages) } else { - Log.w(TAG, "No ETag header in response") + Log.w(InAppMessages.TAG, "[Repository] No ETag header in response") } messages @@ -55,33 +52,33 @@ internal class InAppMessageRepositoryImpl( 304 -> { // Data not modified - use cached messages if (debug) { - Log.d(TAG, "Received 304 Not Modified, using cached messages") + Log.d(InAppMessages.TAG, "[Repository] Received 304 Not Modified, using cached messages") } val cachedMessages = persistence.getCachedMessages() if (cachedMessages != null) { cachedMessages } else { - Log.w(TAG, "304 response but no cached messages found - clearing cache") + Log.w(InAppMessages.TAG, "[Repository] 304 response but no cached messages found - clearing cache") persistence.clearCache() emptyList() } } else -> { - Log.e(TAG, "Error fetching messages from API: ${response.code()}") + Log.e(InAppMessages.TAG, "[Repository] Error fetching messages from API: ${response.code()}") // On error, try to return cached messages if available val cachedMessages = persistence.getCachedMessages() if (debug && cachedMessages != null) { - Log.d(TAG, "API error, falling back to ${cachedMessages.size} cached messages") + Log.d(InAppMessages.TAG, "[Repository] API error, falling back to ${cachedMessages.size} cached messages") } cachedMessages ?: emptyList() } } } } catch (e: Exception) { - Log.e(TAG, "Exception fetching messages from API", e) + Log.e(InAppMessages.TAG, "[Repository] Exception fetching messages from API", e) // On network error, try to return cached messages if available val cachedMessages = persistence.getCachedMessages() diff --git a/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/CustomCodeHandler.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/CustomCodeHandler.kt new file mode 100644 index 00000000..092a057f --- /dev/null +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/CustomCodeHandler.kt @@ -0,0 +1,5 @@ +package com.pushpushgo.sdk.inapp.ui + +interface CustomCodeHandler { + fun handle(code: String) +} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayer.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayer.kt similarity index 73% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayer.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayer.kt index 2cd46806..9522f7f5 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayer.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayer.kt @@ -1,7 +1,7 @@ -package com.pushpushgo.inappmessages.ui +package com.pushpushgo.sdk.inapp.ui import android.app.Activity -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage internal interface InAppMessageDisplayer { fun showMessage( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayerImpl.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayerImpl.kt similarity index 75% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayerImpl.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayerImpl.kt index 60cf7633..5aa4dc0d 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppMessageDisplayerImpl.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppMessageDisplayerImpl.kt @@ -1,9 +1,10 @@ -package com.pushpushgo.inappmessages.ui +package com.pushpushgo.sdk.inapp.ui import android.app.Activity import android.app.Dialog import android.content.Context import android.content.Intent +import android.graphics.Color import android.util.Log import android.view.Gravity import android.view.ViewGroup @@ -12,21 +13,23 @@ import android.widget.Toast import androidx.compose.ui.platform.ComposeView import androidx.core.graphics.drawable.toDrawable import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import com.pushpushgo.inappmessages.R -import com.pushpushgo.inappmessages.model.AnimationType -import com.pushpushgo.inappmessages.model.InAppActionType -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.model.ShowAgainType -import com.pushpushgo.inappmessages.persistence.InAppMessagePersistence -import com.pushpushgo.inappmessages.ui.composables.templates.InAppMessageDefaultTemplate -import com.pushpushgo.inappmessages.ui.composables.templates.TemplateBannerMessage -import com.pushpushgo.inappmessages.ui.composables.templates.TemplateReviewForDiscount -import com.pushpushgo.inappmessages.ui.composables.templates.TemplateRichMessage -import com.pushpushgo.inappmessages.utils.DefaultPushNotificationSubscriber -import com.pushpushgo.inappmessages.utils.PushNotificationSubscriber +import com.pushpushgo.sdk.core.api.PushSubscriptionProvider +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.R +import com.pushpushgo.sdk.inapp.model.AnimationType +import com.pushpushgo.sdk.inapp.model.InAppActionType +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.model.ShowAgainType +import com.pushpushgo.sdk.inapp.persistence.InAppMessagePersistence +import com.pushpushgo.sdk.inapp.ui.composables.templates.InAppMessageDefaultTemplate +import com.pushpushgo.sdk.inapp.ui.composables.templates.TemplateBannerMessage +import com.pushpushgo.sdk.inapp.ui.composables.templates.TemplateReviewForDiscount +import com.pushpushgo.sdk.inapp.ui.composables.templates.TemplateRichMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -44,12 +47,10 @@ internal class InAppMessageDisplayerImpl( private val debug: Boolean = false, private val onMessageDismissed: () -> Unit, private val onMessageEvent: (eventType: String, message: InAppMessage, ctaIndex: Int?) -> Unit = { _, _, _ -> }, - private var onJsAction: ((jsCall: String) -> Unit)? = null, - private var subscriptionHandler: PushNotificationSubscriber = DefaultPushNotificationSubscriber(), + private val pushSubscriptionProvider: PushSubscriptionProvider? = null, + private val customCodeHandler: CustomCodeHandler? = null, ) : InAppMessageDisplayer, CoroutineScope { - private val tag = "InAppMsgDisplayer" - // Coroutine context and job for managing message display jobs private val job = SupervisorJob() override val coroutineContext: CoroutineContext @@ -76,7 +77,7 @@ internal class InAppMessageDisplayerImpl( if (delaySec > 0) { val delayMs = delaySec * 1000L if (debug) { - Log.d(tag, "Scheduling message [${message.id}] with ${delayMs}ms delay") + Log.d(InAppMessages.TAG, "[Displayer] Scheduling message [${message.id}] with ${delayMs}ms delay") } val activityRef = WeakReference(activity) @@ -103,11 +104,11 @@ internal class InAppMessageDisplayerImpl( } catch (t: Throwable) { if (t is CancellationException) { if (debug) { - Log.d(tag, "Message [${message.id}] display cancelled") + Log.d(InAppMessages.TAG, "[Displayer] Message [${message.id}] display cancelled") } throw t } - Log.e(tag, "Failed to display message ${message.id}", t) + Log.e(InAppMessages.TAG, "[Displayer] Failed to display message ${message.id}", t) } finally { // Always remove the job from the map when it's done. // This check prevents a race condition where a new job for the same ID is added @@ -120,7 +121,7 @@ internal class InAppMessageDisplayerImpl( pendingMessageJobs[message.id] = message to newJob } else { if (debug) { - Log.d(tag, "Showing message [${message.id}] immediately") + Log.d(InAppMessages.TAG, "[Displayer] Showing message [${message.id}] immediately") } launch { if (shouldBeDisplayed(message)) { @@ -141,7 +142,9 @@ internal class InAppMessageDisplayerImpl( when (message.template) { "WEBSITE_TO_HOME_SCREEN", "PAYWALL_PUBLISH", - -> R.style.InAppMessageDialog_Modal + -> { + R.style.InAppMessageDialog_Modal + } "EXIT_INTENT_ECOMM", "PUSH_NOTIFICATION_OPT_IN", @@ -149,10 +152,12 @@ internal class InAppMessageDisplayerImpl( "UNBLOCK_NOTIFICATIONS", "LOW_STOCK", "REVIEW_FOR_DISCOUNT", - -> R.style.InAppMessageDialog_Banner + -> { + R.style.InAppMessageDialog_Banner + } else -> { - Log.w(tag, "Unsupported template: ${message.template}, no container style defined.") + Log.w(InAppMessages.TAG, "[Displayer] Unsupported template: ${message.template}, no container style defined.") null } } @@ -163,11 +168,18 @@ internal class InAppMessageDisplayerImpl( } override fun cancelPendingMessages(isActivityPaused: Boolean) { + if (isActivityPaused) { + hideMessage() + } + if (pendingMessageJobs.isEmpty()) { return } if (debug) { - Log.d(tag, "Cancelling ${pendingMessageJobs.size} pending message jobs (activity paused: $isActivityPaused)") + Log.d( + InAppMessages.TAG, + "[Displayer] Cancelling ${pendingMessageJobs.size} pending message jobs (activity paused: $isActivityPaused)", + ) } // Create a copy of the values to avoid ConcurrentModificationException @@ -181,7 +193,13 @@ internal class InAppMessageDisplayerImpl( private fun hideMessage() { currentDialog?.let { if (it.isShowing) { - it.dismiss() + try { + it.dismiss() + } catch (e: Exception) { + if (debug) { + Log.w(InAppMessages.TAG, "[Displayer] Failed to dismiss dialog: ${e.message}") + } + } } } currentDialog = null @@ -189,7 +207,7 @@ internal class InAppMessageDisplayerImpl( override fun dismissMessage(message: InAppMessage) { if (debug) { - Log.d(tag, "Dismissing message [${message.id}]") + Log.d(InAppMessages.TAG, "[Displayer] Dismissing message [${message.id}]") } dismissMessageInternal(message, sendCloseEvent = true) } @@ -260,7 +278,7 @@ internal class InAppMessageDisplayerImpl( // Handle overlay property - set window background explicitly based on overlay setting window?.setBackgroundDrawable( - android.graphics.Color.TRANSPARENT + Color.TRANSPARENT .toDrawable(), ) @@ -287,6 +305,7 @@ internal class InAppMessageDisplayerImpl( message.layout.placement .toString() .startsWith("TOP") -> Gravity.TOP or Gravity.CENTER_HORIZONTAL + message.layout.placement .toString() .startsWith("BOTTOM") -> Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL @@ -299,6 +318,13 @@ internal class InAppMessageDisplayerImpl( } setOnDismissListener { if (currentDialog == this) dismissMessage(message) } } + if (activity.isFinishing || activity.isDestroyed) { + if (debug) { + Log.d(InAppMessages.TAG, "[Displayer] Activity is finishing/destroyed, skipping dialog.show()") + } + return@withContext + } + dialog.show() currentDialog = dialog // Fire show event after dialog is visible @@ -323,8 +349,8 @@ internal class InAppMessageDisplayerImpl( // This is crucial for Jetpack Compose to work correctly in a Dialog or any view // that is not directly part of the Activity's main content view, as it allows // the composable to observe LiveData, use ViewModels, and save instance state. - setViewTreeLifecycleOwner(activity as? androidx.lifecycle.LifecycleOwner) - setViewTreeSavedStateRegistryOwner(activity as? androidx.savedstate.SavedStateRegistryOwner) + setViewTreeLifecycleOwner(activity as? LifecycleOwner) + setViewTreeSavedStateRegistryOwner(activity as? SavedStateRegistryOwner) layoutParams = FrameLayout @@ -385,26 +411,6 @@ internal class InAppMessageDisplayerImpl( } } - /** - * Sets a handler for code from actions. - * This method allows updating the code action handler after initialization. - * - * @param handler Function that takes button action code string and processes it - */ - internal fun setJsActionHandler(handler: (jsCall: String) -> Unit) { - this.onJsAction = handler - } - - /** - * Sets a handler for subscription requests. - * This will be called when a SUBSCRIBE action button is clicked. - * - * @param handler The PushNotificationSubscriber implementation - */ - internal fun setSubscriptionHandler(handler: PushNotificationSubscriber) { - this.subscriptionHandler = handler - } - private fun handleAction( context: Context, action: InAppMessageAction, @@ -418,40 +424,38 @@ internal class InAppMessageDisplayerImpl( addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, ) - } ?: Log.e(tag, "URL is null or empty in REDIRECT action") + } ?: Log.e(InAppMessages.TAG, "[Displayer] URL is null or empty in REDIRECT action") } InAppActionType.JS -> { - action.call?.takeIf { it.isNotEmpty() }?.let { jsCall -> - onJsAction?.invoke(jsCall) ?: run { - Log.w(tag, "No JS action handler provided for call: $jsCall") - } - } ?: Log.e(tag, "JS call value is null or empty") + action.call?.takeIf { it.isNotEmpty() }?.let { code -> + customCodeHandler?.handle(code) + } ?: Log.e(InAppMessages.TAG, "[Displayer] Custom code is null or empty") } InAppActionType.SUBSCRIBE -> { - val success = + launch { + if (pushSubscriptionProvider == null) { + Log.e(InAppMessages.TAG, "[Displayer] No PushSubscriptionProvider configured - cannot subscribe to notifications") + return@launch + } + + if (pushSubscriptionProvider.isSubscribed()) { + Log.i(InAppMessages.TAG, "[Displayer] Already subscribed to notifications, skipping") + return@launch + } + try { - subscriptionHandler.requestSubscription(context) + pushSubscriptionProvider.subscribe() } catch (e: Exception) { - Log.e(tag, "Error processing subscription request", e) - false + Log.e(InAppMessages.TAG, "[Displayer] Error subscribing to notifications", e) } - if (success) { - Toast - .makeText( - context, - "Successfully subscribed to notifications!", - Toast.LENGTH_SHORT, - ).show() - } else { - Toast - .makeText( - context, - "Subscription failed. Enable notifications in settings.", - Toast.LENGTH_LONG, - ).show() + if (pushSubscriptionProvider.isSubscribed()) { + Toast.makeText(context, "Successfully subscribed to notifications!", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Subscription failed. Enable notifications in settings.", Toast.LENGTH_LONG).show() + } } } @@ -461,7 +465,7 @@ internal class InAppMessageDisplayerImpl( } } } catch (e: Exception) { - Log.e(tag, "Failed to handle action", e) + Log.e(InAppMessages.TAG, "[Displayer] Failed to handle action", e) Toast.makeText(context, "Error performing action", Toast.LENGTH_SHORT).show() } } diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppUIController.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppUIController.kt similarity index 77% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppUIController.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppUIController.kt index 504895d2..16e4ee07 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/InAppUIController.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/InAppUIController.kt @@ -1,11 +1,12 @@ -package com.pushpushgo.inappmessages.ui +package com.pushpushgo.sdk.inapp.ui import android.app.Activity import android.app.Application import android.os.Bundle import android.util.Log -import com.pushpushgo.inappmessages.manager.InAppMessageManager -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.InAppMessages +import com.pushpushgo.sdk.inapp.manager.InAppMessageManager +import com.pushpushgo.sdk.inapp.model.InAppMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -23,8 +24,6 @@ internal class InAppUIController( private val debug: Boolean = false, ) : Application.ActivityLifecycleCallbacks, CoroutineScope { - private val tag = "InAppUIController" - private val job = SupervisorJob() override val coroutineContext = Dispatchers.Main + job @@ -33,21 +32,15 @@ internal class InAppUIController( fun start() { if (debug) { - Log.d(tag, "Starting InApp UI Controller") + Log.d(InAppMessages.TAG, "[UIController] Starting") } application.registerActivityLifecycleCallbacks(this) observeMessages() } - fun stop() { - application.unregisterActivityLifecycleCallbacks(this) - job.cancel() - messageSubscription?.cancel() - } - private fun observeMessages() { if (debug) { - Log.d(tag, "Starting to observe messages flow") + Log.d(InAppMessages.TAG, "[UIController] Starting to observe messages flow") } messageSubscription?.cancel() messageSubscription = @@ -57,20 +50,23 @@ internal class InAppUIController( val activity = currentActivity.get() if (activity == null || activity.isFinishing) { if (debug) { - Log.d(tag, "No current activity available, skipping message display") + Log.d(InAppMessages.TAG, "[UIController] No current activity available, skipping message display") } return@onEach } if (messages.isNotEmpty()) { if (debug) { - Log.d(tag, "Displaying message from flow: [${messages.first().id}] (${messages.size} total messages available)") + Log.d( + InAppMessages.TAG, + "[UIController] Displaying message from flow: [${messages.first().id}] (${messages.size} total messages available)", + ) } val highestPriorityMessage = messages.first() displayer.showMessage(activity, highestPriorityMessage) } else { if (debug) { - Log.d(tag, "No messages to display") + Log.d(InAppMessages.TAG, "[UIController] No messages to display") } displayer.cancelPendingMessages(isActivityPaused = false) } @@ -79,12 +75,13 @@ internal class InAppUIController( override fun onActivityResumed(activity: Activity) { if (debug) { - Log.d(tag, "Activity resumed: ${activity.javaClass.simpleName}") + Log.d(InAppMessages.TAG, "[UIController] Activity resumed: ${activity.javaClass.simpleName}") } + currentActivity = WeakReference(activity) launch { - manager.refreshActiveMessages() + manager.refreshActiveMessages(manager.getRoute()) // Force display of available messages after activity resume // This handles the case where distinctUntilChanged() blocks emission // of the same message list after permission for push notifications changes @@ -99,10 +96,12 @@ internal class InAppUIController( override fun onActivityPaused(activity: Activity) { if (currentActivity.get() == activity) { if (debug) { - Log.d(tag, "Activity paused, cancelling pending messages") + Log.d(InAppMessages.TAG, "[UIController] Activity paused, cancelling pending messages") } + currentActivity.clear() } + displayer.cancelPendingMessages(isActivityPaused = true) } @@ -125,6 +124,7 @@ internal class InAppUIController( fun displayCustomMessage(message: InAppMessage) { launch { val activity = currentActivity.get() + if (activity == null || activity.isFinishing) { return@launch } diff --git a/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/Trigger.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/Trigger.kt new file mode 100644 index 00000000..6a3d6a0e --- /dev/null +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/Trigger.kt @@ -0,0 +1,40 @@ +package com.pushpushgo.sdk.inapp.ui + +class Trigger private constructor( + val key: String, + val value: String? = null, +) { + init { + require(key.isNotBlank()) { + "Trigger key must not be blank" + } + + require(value?.isNotBlank() ?: true) { + "Trigger value must not be blank" + } + } + + companion object { + /** + * Creates a key-only trigger. + * + * @param key Non-blank trigger identifier. + * + * @throws IllegalArgumentException if key is blank. + */ + fun key(key: String) = Trigger(key) + + /** + * Creates a key–value trigger. + * + * @param key Non-blank trigger identifier. + * @param value Non-blank trigger value. + * + * @throws IllegalArgumentException if key or value is blank. + */ + fun keyValue( + key: String, + value: String, + ) = Trigger(key, value) + } +} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageButton.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageButton.kt similarity index 94% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageButton.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageButton.kt index d4b37404..a9d36f4b 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageButton.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageButton.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -28,9 +28,9 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em -import com.pushpushgo.inappmessages.model.FontStyle -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.model.InAppMessageStyle +import com.pushpushgo.sdk.inapp.model.FontStyle +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.model.InAppMessageStyle @Composable internal fun MessageButtons( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCard.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCard.kt similarity index 88% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCard.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCard.kt index 78978d62..139c2c4d 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCard.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCard.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import androidx.compose.foundation.BorderStroke import androidx.compose.material3.Card @@ -6,7 +6,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage @Composable internal fun MessageCard( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCardShadow.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCardShadow.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCardShadow.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCardShadow.kt index 4f654036..1c41b366 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageCardShadow.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageCardShadow.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import android.graphics.BlurMaskFilter import androidx.compose.foundation.layout.Box @@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb -import com.pushpushgo.inappmessages.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessage @Composable internal fun MessageCardShadow( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageImage.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageImage.kt similarity index 82% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageImage.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageImage.kt index 6595ce99..8638b231 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageImage.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageImage.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -8,8 +8,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import coil.compose.AsyncImage -import com.pushpushgo.inappmessages.model.InAppMessageImage -import com.pushpushgo.inappmessages.model.InAppMessageStyle +import com.pushpushgo.sdk.inapp.model.InAppMessageImage +import com.pushpushgo.sdk.inapp.model.InAppMessageStyle @Composable internal fun MessageImage( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageText.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageText.kt similarity index 88% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageText.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageText.kt index 812e86c7..17bad897 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/MessageText.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/MessageText.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text @@ -9,9 +9,9 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration -import com.pushpushgo.inappmessages.model.FontStyle -import com.pushpushgo.inappmessages.model.InAppMessageDescription -import com.pushpushgo.inappmessages.model.InAppMessageTitle +import com.pushpushgo.sdk.inapp.model.FontStyle +import com.pushpushgo.sdk.inapp.model.InAppMessageDescription +import com.pushpushgo.sdk.inapp.model.InAppMessageTitle @Composable internal fun MessageTitle( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/Utils.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/Utils.kt similarity index 75% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/Utils.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/Utils.kt index 40563ff4..cacb2af7 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/common/Utils.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/common/Utils.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.common +package com.pushpushgo.sdk.inapp.ui.composables.common import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding @@ -19,12 +19,12 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt -import com.pushpushgo.inappmessages.R -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.model.InAppMessageDescription -import com.pushpushgo.inappmessages.model.InAppMessageStyle -import com.pushpushgo.inappmessages.model.InAppMessageTitle +import com.pushpushgo.sdk.inapp.R +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.model.InAppMessageDescription +import com.pushpushgo.sdk.inapp.model.InAppMessageStyle +import com.pushpushgo.sdk.inapp.model.InAppMessageTitle internal fun Color.Companion.fromHex(hex: String): Color = try { @@ -45,32 +45,40 @@ internal fun parseBorderRadius(borderRadius: String?): Shape { val parts = removePxFromString(borderRadius) return when (parts.size) { - 1 -> RoundedCornerShape(parts[0].pxToDp) - 2 -> + 1 -> { + RoundedCornerShape(parts[0].pxToDp) + } + + 2 -> { RoundedCornerShape( topStart = parts[0].pxToDp, topEnd = parts[0].pxToDp, bottomStart = parts[1].pxToDp, bottomEnd = parts[1].pxToDp, ) + } - 3 -> + 3 -> { RoundedCornerShape( topStart = parts[0].pxToDp, topEnd = parts[1].pxToDp, bottomStart = parts[2].pxToDp, bottomEnd = parts[1].pxToDp, ) + } - 4 -> + 4 -> { RoundedCornerShape( topStart = parts[0].pxToDp, topEnd = parts[1].pxToDp, bottomStart = parts[3].pxToDp, bottomEnd = parts[2].pxToDp, ) + } - else -> RoundedCornerShape(0.pxToDp) + else -> { + RoundedCornerShape(0.pxToDp) + } } } @@ -80,25 +88,35 @@ internal fun parsePadding(padding: String?): PaddingValues { val parts = removePxFromString(padding) return when (parts.size) { - 1 -> PaddingValues(parts[0].pxToDp) - 2 -> PaddingValues(vertical = parts[0].pxToDp, horizontal = parts[1].pxToDp) - 3 -> + 1 -> { + PaddingValues(parts[0].pxToDp) + } + + 2 -> { + PaddingValues(vertical = parts[0].pxToDp, horizontal = parts[1].pxToDp) + } + + 3 -> { PaddingValues( top = parts[0].pxToDp, start = parts[1].pxToDp, end = parts[1].pxToDp, bottom = parts[2].pxToDp, ) + } - 4 -> + 4 -> { PaddingValues( top = parts[0].pxToDp, end = parts[1].pxToDp, bottom = parts[2].pxToDp, start = parts[3].pxToDp, ) + } - else -> PaddingValues(0.pxToDp) + else -> { + PaddingValues(0.pxToDp) + } } } @@ -148,16 +166,16 @@ internal fun createFontFamily(message: InAppMessage): FontFamily { val fontName = when (message.style.fontFamily) { - com.pushpushgo.inappmessages.model.FontFamily.ROBOTO -> "Roboto" - com.pushpushgo.inappmessages.model.FontFamily.OPEN_SANS -> "Open Sans" - com.pushpushgo.inappmessages.model.FontFamily.MONTSERRAT -> "Montserrat" - com.pushpushgo.inappmessages.model.FontFamily.INTER -> "Inter" - com.pushpushgo.inappmessages.model.FontFamily.POPPINS -> "Poppins" - com.pushpushgo.inappmessages.model.FontFamily.LATO -> "Lato" - com.pushpushgo.inappmessages.model.FontFamily.PLAYFAIR_DISPLAY -> "Playfair Display" - com.pushpushgo.inappmessages.model.FontFamily.FIRA_SANS -> "Fira Sans" - com.pushpushgo.inappmessages.model.FontFamily.ARIAL -> "Roboto" - com.pushpushgo.inappmessages.model.FontFamily.GEORGIA -> "Gelasio" + com.pushpushgo.sdk.inapp.model.FontFamily.ROBOTO -> "Roboto" + com.pushpushgo.sdk.inapp.model.FontFamily.OPEN_SANS -> "Open Sans" + com.pushpushgo.sdk.inapp.model.FontFamily.MONTSERRAT -> "Montserrat" + com.pushpushgo.sdk.inapp.model.FontFamily.INTER -> "Inter" + com.pushpushgo.sdk.inapp.model.FontFamily.POPPINS -> "Poppins" + com.pushpushgo.sdk.inapp.model.FontFamily.LATO -> "Lato" + com.pushpushgo.sdk.inapp.model.FontFamily.PLAYFAIR_DISPLAY -> "Playfair Display" + com.pushpushgo.sdk.inapp.model.FontFamily.FIRA_SANS -> "Fira Sans" + com.pushpushgo.sdk.inapp.model.FontFamily.ARIAL -> "Roboto" + com.pushpushgo.sdk.inapp.model.FontFamily.GEORGIA -> "Gelasio" } val fontWeights = @@ -180,7 +198,7 @@ internal fun createFontFamily(message: InAppMessage): FontFamily { fontProvider = provider, weight = FontWeight(weight), style = - if (style == com.pushpushgo.inappmessages.model.FontStyle.ITALIC) { + if (style == com.pushpushgo.sdk.inapp.model.FontStyle.ITALIC) { FontStyle.Italic } else { FontStyle.Normal diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/BannerMessageTemplate.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/BannerMessageTemplate.kt similarity index 82% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/BannerMessageTemplate.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/BannerMessageTemplate.kt index 6403694f..673bacb9 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/BannerMessageTemplate.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/BannerMessageTemplate.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.templates +package com.pushpushgo.sdk.inapp.ui.composables.templates import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,17 +16,17 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import coil.compose.AsyncImage -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.ui.composables.common.CloseButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageCard -import com.pushpushgo.inappmessages.ui.composables.common.MessageCardShadow -import com.pushpushgo.inappmessages.ui.composables.common.MessageDescription -import com.pushpushgo.inappmessages.ui.composables.common.MessageTitle -import com.pushpushgo.inappmessages.ui.composables.common.createFontFamily -import com.pushpushgo.inappmessages.ui.composables.common.parsePadding -import com.pushpushgo.inappmessages.ui.composables.common.pxToDp +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.ui.composables.common.CloseButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageCard +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageCardShadow +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageDescription +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageTitle +import com.pushpushgo.sdk.inapp.ui.composables.common.createFontFamily +import com.pushpushgo.sdk.inapp.ui.composables.common.parsePadding +import com.pushpushgo.sdk.inapp.ui.composables.common.pxToDp /** * Banner-style message template for: @@ -57,7 +57,7 @@ internal fun TemplateBannerMessage( ) } - Box(modifier = Modifier.padding(parsePadding(message.layout.padding))) { + Box(modifier = Modifier.Companion.padding(parsePadding(message.layout.padding))) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/InAppMessageDefaultTemplate.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/InAppMessageDefaultTemplate.kt similarity index 79% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/InAppMessageDefaultTemplate.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/InAppMessageDefaultTemplate.kt index 66159b0f..e0bdf4b1 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/InAppMessageDefaultTemplate.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/InAppMessageDefaultTemplate.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.templates +package com.pushpushgo.sdk.inapp.ui.composables.templates import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -17,17 +17,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.ui.composables.common.CloseButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageButtons -import com.pushpushgo.inappmessages.ui.composables.common.MessageDescription -import com.pushpushgo.inappmessages.ui.composables.common.MessageImage -import com.pushpushgo.inappmessages.ui.composables.common.MessageTitle -import com.pushpushgo.inappmessages.ui.composables.common.fromHex -import com.pushpushgo.inappmessages.ui.composables.common.parseBorderRadius -import com.pushpushgo.inappmessages.ui.composables.common.parsePadding -import com.pushpushgo.inappmessages.ui.composables.common.pxToDp +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.ui.composables.common.CloseButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageButtons +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageDescription +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageImage +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageTitle +import com.pushpushgo.sdk.inapp.ui.composables.common.fromHex +import com.pushpushgo.sdk.inapp.ui.composables.common.parseBorderRadius +import com.pushpushgo.sdk.inapp.ui.composables.common.parsePadding +import com.pushpushgo.sdk.inapp.ui.composables.common.pxToDp @Composable internal fun InAppMessageDefaultTemplate( diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/ReviewForDiscountTemplate.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/ReviewForDiscountTemplate.kt similarity index 81% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/ReviewForDiscountTemplate.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/ReviewForDiscountTemplate.kt index 6ed5167e..06bd7659 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/ReviewForDiscountTemplate.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/ReviewForDiscountTemplate.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.templates +package com.pushpushgo.sdk.inapp.ui.composables.templates import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,19 +16,19 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import coil.compose.AsyncImage -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.model.Placement -import com.pushpushgo.inappmessages.ui.composables.common.CloseButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageCard -import com.pushpushgo.inappmessages.ui.composables.common.MessageCardShadow -import com.pushpushgo.inappmessages.ui.composables.common.MessageDescription -import com.pushpushgo.inappmessages.ui.composables.common.MessageTitle -import com.pushpushgo.inappmessages.ui.composables.common.add -import com.pushpushgo.inappmessages.ui.composables.common.createFontFamily -import com.pushpushgo.inappmessages.ui.composables.common.parsePadding -import com.pushpushgo.inappmessages.ui.composables.common.pxToDp +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.model.Placement +import com.pushpushgo.sdk.inapp.ui.composables.common.CloseButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageCard +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageCardShadow +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageDescription +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageTitle +import com.pushpushgo.sdk.inapp.ui.composables.common.add +import com.pushpushgo.sdk.inapp.ui.composables.common.createFontFamily +import com.pushpushgo.sdk.inapp.ui.composables.common.parsePadding +import com.pushpushgo.sdk.inapp.ui.composables.common.pxToDp /** * Banner-style message template for: @@ -98,7 +98,7 @@ internal fun TemplateReviewForDiscount( Row( modifier = - Modifier + Modifier.Companion .padding(parsePadding(message.layout.paddingBody)) .weight(1f), ) { diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/RichMessageTemplate.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/RichMessageTemplate.kt similarity index 79% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/RichMessageTemplate.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/RichMessageTemplate.kt index d6a0a2c7..b52516fe 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/ui/composables/templates/RichMessageTemplate.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/ui/composables/templates/RichMessageTemplate.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.ui.composables.templates +package com.pushpushgo.sdk.inapp.ui.composables.templates import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,16 +15,16 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import coil.compose.AsyncImage -import com.pushpushgo.inappmessages.model.InAppMessage -import com.pushpushgo.inappmessages.model.InAppMessageAction -import com.pushpushgo.inappmessages.ui.composables.common.CloseButton -import com.pushpushgo.inappmessages.ui.composables.common.MessageButtons -import com.pushpushgo.inappmessages.ui.composables.common.MessageCard -import com.pushpushgo.inappmessages.ui.composables.common.MessageDescription -import com.pushpushgo.inappmessages.ui.composables.common.MessageTitle -import com.pushpushgo.inappmessages.ui.composables.common.createFontFamily -import com.pushpushgo.inappmessages.ui.composables.common.parsePadding -import com.pushpushgo.inappmessages.ui.composables.common.pxToDp +import com.pushpushgo.sdk.inapp.model.InAppMessage +import com.pushpushgo.sdk.inapp.model.InAppMessageAction +import com.pushpushgo.sdk.inapp.ui.composables.common.CloseButton +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageButtons +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageCard +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageDescription +import com.pushpushgo.sdk.inapp.ui.composables.common.MessageTitle +import com.pushpushgo.sdk.inapp.ui.composables.common.createFontFamily +import com.pushpushgo.sdk.inapp.ui.composables.common.parsePadding +import com.pushpushgo.sdk.inapp.ui.composables.common.pxToDp /** * Banner-style message template for: diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/DeviceInfoProvider.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/DeviceInfoProvider.kt similarity index 77% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/DeviceInfoProvider.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/DeviceInfoProvider.kt index 56282efb..f82567a1 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/DeviceInfoProvider.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/DeviceInfoProvider.kt @@ -1,9 +1,9 @@ -package com.pushpushgo.inappmessages.utils +package com.pushpushgo.sdk.inapp.utils import android.content.Context import android.content.res.Configuration -import com.pushpushgo.inappmessages.model.DeviceType -import com.pushpushgo.inappmessages.model.OSType +import com.pushpushgo.sdk.inapp.model.DeviceType +import com.pushpushgo.sdk.inapp.model.OSType internal object DeviceInfoProvider { fun getCurrentDeviceType(context: Context): DeviceType { diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/InAppMessageHelper.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/InAppMessageHelper.kt similarity index 92% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/InAppMessageHelper.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/InAppMessageHelper.kt index 8cd9b128..577102cb 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/InAppMessageHelper.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/InAppMessageHelper.kt @@ -1,12 +1,13 @@ -package com.pushpushgo.inappmessages.utils +package com.pushpushgo.sdk.inapp.utils import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import com.pushpushgo.inappmessages.InAppMessagesSDK +import com.pushpushgo.sdk.inapp.InAppMessages /** * Helper class that simplifies in-app message integration across different UI frameworks. @@ -20,7 +21,7 @@ object InAppMessageHelper { * @param screenName The name of the current screen/route */ fun showMessagesForScreen(screenName: String) { - InAppMessagesSDK.getInstance().showActiveMessages(screenName) + InAppMessages.getInstance().showMessagesOnRoute(screenName) } /** @@ -109,8 +110,8 @@ object InAppMessageHelper { */ @Composable fun ObserveNavBackStack( - navBackStackEntry: androidx.navigation.NavBackStackEntry?, - routeProvider: (androidx.navigation.NavBackStackEntry?) -> String? = { it?.destination?.route }, + navBackStackEntry: NavBackStackEntry?, + routeProvider: (NavBackStackEntry?) -> String? = { it?.destination?.route }, ) { val currentRoute = routeProvider(navBackStackEntry) ObserveNavigation(currentRoute) diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/ZonedDateTimeAdapter.kt b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/ZonedDateTimeAdapter.kt similarity index 96% rename from library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/ZonedDateTimeAdapter.kt rename to inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/ZonedDateTimeAdapter.kt index 403faf3e..a891ac9f 100644 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/ZonedDateTimeAdapter.kt +++ b/inapp/src/main/java/com/pushpushgo/sdk/inapp/utils/ZonedDateTimeAdapter.kt @@ -1,4 +1,4 @@ -package com.pushpushgo.inappmessages.utils +package com.pushpushgo.sdk.inapp.utils import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader diff --git a/library-inappmessages/src/main/res/anim/fade_in.xml b/inapp/src/main/res/anim/fade_in.xml similarity index 100% rename from library-inappmessages/src/main/res/anim/fade_in.xml rename to inapp/src/main/res/anim/fade_in.xml diff --git a/library-inappmessages/src/main/res/values/font_certs.xml b/inapp/src/main/res/values/font_certs.xml similarity index 100% rename from library-inappmessages/src/main/res/values/font_certs.xml rename to inapp/src/main/res/values/font_certs.xml diff --git a/library-inappmessages/src/main/res/values/styles.xml b/inapp/src/main/res/values/styles.xml similarity index 100% rename from library-inappmessages/src/main/res/values/styles.xml rename to inapp/src/main/res/values/styles.xml diff --git a/library-inappmessages/src/test/java/com/pushpushgo/inappmessages/ExampleUnitTest.kt b/inapp/src/test/java/com/pushpushgo/sdk/inapp/ExampleUnitTest.kt similarity index 73% rename from library-inappmessages/src/test/java/com/pushpushgo/inappmessages/ExampleUnitTest.kt rename to inapp/src/test/java/com/pushpushgo/sdk/inapp/ExampleUnitTest.kt index 58d0975d..f1fb9951 100644 --- a/library-inappmessages/src/test/java/com/pushpushgo/inappmessages/ExampleUnitTest.kt +++ b/inapp/src/test/java/com/pushpushgo/sdk/inapp/ExampleUnitTest.kt @@ -1,6 +1,6 @@ -package com.pushpushgo.inappmessages +package com.pushpushgo.sdk.inapp -import org.junit.Assert.* +import org.junit.Assert import org.junit.Test /** @@ -11,6 +11,6 @@ import org.junit.Test class ExampleUnitTest { @Test fun addition_isCorrect() { - assertEquals(4, 2 + 2) + Assert.assertEquals(4, 2 + 2) } } diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index efde7bf2..00000000 --- a/jitpack.yml +++ /dev/null @@ -1,2 +0,0 @@ -jdk: - - openjdk17 diff --git a/keystore.jks b/keystore.jks deleted file mode 100644 index bbfad760..00000000 Binary files a/keystore.jks and /dev/null differ diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/InAppMessagesSDK.kt b/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/InAppMessagesSDK.kt deleted file mode 100644 index 782f587f..00000000 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/InAppMessagesSDK.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.pushpushgo.inappmessages - -import android.app.Application -import android.util.Log -import com.pushpushgo.inappmessages.data.event.InAppMessageEvent -import com.pushpushgo.inappmessages.data.event.InAppMessageEventRepository -import com.pushpushgo.inappmessages.manager.InAppMessageManager -import com.pushpushgo.inappmessages.manager.InAppMessageManagerImpl -import com.pushpushgo.inappmessages.network.InAppEventApi -import com.pushpushgo.inappmessages.network.InAppListGetApi -import com.pushpushgo.inappmessages.network.RetrofitProvider -import com.pushpushgo.inappmessages.persistence.InAppMessagePersistenceImpl -import com.pushpushgo.inappmessages.repository.InAppMessageRepositoryImpl -import com.pushpushgo.inappmessages.ui.InAppMessageDisplayer -import com.pushpushgo.inappmessages.ui.InAppMessageDisplayerImpl -import com.pushpushgo.inappmessages.ui.InAppUIController -import com.pushpushgo.inappmessages.utils.AutoCleanupManager -import com.pushpushgo.inappmessages.utils.DefaultPushNotificationSubscriber -import com.pushpushgo.inappmessages.utils.PushNotificationSubscriber -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import retrofit2.Retrofit - -class InAppMessagesSDK private constructor( - private val application: Application, - private val projectId: String, - private val apiKey: String, - private val debug: Boolean = false, - private val baseUrl: String? = null, - private var pushNotificationSubscriber: PushNotificationSubscriber = DefaultPushNotificationSubscriber(), -) { - // --- Retrofit & APIs --- - private val retrofit: Retrofit by lazy { - RetrofitProvider.buildRetrofit(baseUrl ?: "https://api.pushpushgo.com/") - } - private val api: InAppListGetApi by lazy { - retrofit.create(InAppListGetApi::class.java) - } - private val eventApi: InAppEventApi by lazy { - retrofit.create(InAppEventApi::class.java) - } - private val eventRepository by lazy { - InAppMessageEventRepository(eventApi, debug = debug) - } - - internal suspend fun dispatchInAppEvent( - action: String, - inAppId: String, - ) { - try { - eventRepository.sendEvent( - token = apiKey, - projectId = projectId, - event = InAppMessageEvent(action = action, inApp = inAppId), - ) - } catch (e: Exception) { - if (debug) { - Log.e(tag, "Failed to send in-app event", e) - } - } - } - - private val tag = "InAppMessagesSDK" - private val sdkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private val manager: InAppMessageManager - private val displayer: InAppMessageDisplayer - private val uiController: InAppUIController - private var autoCleanupManager: AutoCleanupManager? = null - - companion object { - @Volatile - private var INSTANCE: InAppMessagesSDK? = null - - @JvmStatic - fun initialize( - application: Application, - projectId: String, - apiKey: String, - debug: Boolean = false, - baseUrl: String? = null, - pushNotificationSubscriber: PushNotificationSubscriber? = null, - ): InAppMessagesSDK = - INSTANCE ?: synchronized(this) { - INSTANCE ?: InAppMessagesSDK( - application, - projectId, - apiKey, - debug, - baseUrl, - pushNotificationSubscriber ?: DefaultPushNotificationSubscriber(), - ).also { - INSTANCE = it - } - } - - @JvmStatic - fun getInstance(): InAppMessagesSDK = INSTANCE ?: throw IllegalStateException("InAppMessagesSDK is not initialized!") - } - - init { - val persistence = InAppMessagePersistenceImpl(application, debug) - val repository = InAppMessageRepositoryImpl(api, projectId, apiKey, persistence, debug) - manager = InAppMessageManagerImpl(sdkScope, repository, persistence, application, debug) - displayer = - InAppMessageDisplayerImpl( - persistence, - debug, - onMessageDismissed = { showActiveMessages() }, - onMessageEvent = { eventType, message, ctaIndex -> - sdkScope.launch { - when (eventType) { - "show" -> dispatchInAppEvent("inapp.show", message.id) - "close" -> dispatchInAppEvent("inapp.close", message.id) - "cta" -> dispatchInAppEvent("inapp.cta.$ctaIndex", message.id) - } - } - }, - ) - uiController = InAppUIController(application, manager, displayer, debug) - - sdkScope.launch { - manager.initialize() - } - uiController.start() - - autoCleanupManager = - AutoCleanupManager( - application = application, - cleanupCallback = { cleanup() }, - ) - autoCleanupManager?.start() - } - - /** - * Cleans up resources used by the SDK - * This is called automatically after app is in background for a prolonged period, - * but can also be called manually from app's onDestroy() - */ - private fun cleanup() { - // Stop the auto-cleanup manager - autoCleanupManager?.stop() - autoCleanupManager = null - - uiController.stop() - displayer.cancelPendingMessages() - sdkScope.cancel() - } - - /** - * Shows all in-app messages that should be displayed automatically: - * - If currentRoute is null: shows all messages with trigger.type == APP_OPEN - * - If currentRoute is not null: shows all messages with trigger.type == ROUTE and trigger.route == currentRoute, and all with trigger.type == APP_OPEN - * - * Call this once on app start (with currentRoute = null), - * and on route/view change (with currentRoute = route name). - */ - fun showActiveMessages(currentRoute: String? = null) { - sdkScope.launch { - manager.refreshActiveMessages(currentRoute) - } - } - - /** - * Shows in-app messages for a custom trigger. - * Only messages with trigger.type == CUSTOM_TRIGGER and matching key and value will be shown. - * Also doesn't cancel pending messages for APP_OPEN trigger. - */ - fun showMessagesOnTrigger( - key: String, - value: String, - ) { - sdkScope.launch { - val messageToShow = manager.trigger(key, value) - if (messageToShow != null) { - uiController.displayCustomMessage(messageToShow) - } - } - } - - /** - * Sets a handler for code actions from in-app messages. - * When an in-app message with action type JS is clicked, the handler will be called with the given code. - * - * @param handler Function that takes a action button code string and processes it - */ - fun setJsActionHandler(handler: (jsCall: String) -> Unit) { - (displayer as? InAppMessageDisplayerImpl)?.setJsActionHandler(handler) - } - - /** - * Sets a custom implementation for handling subscription requests. - * This will be called when an in-app message with a SUBSCRIBE action button is clicked. - * - * By default, this SDK attempts to use reflection to find and call the PushPushGo SDK. - * You only need to provide a custom implementation if you're using a different push service - * or have special requirements. - * - * @param subscriber The PushNotificationSubscriber implementation - */ - fun setPushNotificationSubscriber(subscriber: PushNotificationSubscriber) { - this.pushNotificationSubscriber = subscriber - (displayer as? InAppMessageDisplayerImpl)?.setSubscriptionHandler(subscriber) - } -} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepository.kt b/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepository.kt deleted file mode 100644 index 16445e8e..00000000 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/repository/InAppMessageRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.pushpushgo.inappmessages.repository - -import com.pushpushgo.inappmessages.model.InAppMessage - -internal interface InAppMessageRepository { - suspend fun fetchMessages(): List -} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/AutoCleanupManager.kt b/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/AutoCleanupManager.kt deleted file mode 100644 index a2df9233..00000000 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/AutoCleanupManager.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.pushpushgo.inappmessages.utils - -import android.app.Activity -import android.app.Application -import android.os.Bundle -import android.os.Handler -import android.os.Looper - -/** - * Utility class that manages automatic cleanup of SDK resources - * when the app has been in background for a specified amount of time. - * - * This follows the Single Responsibility Principle by extracting - * the lifecycle management logic from the main SDK class. - */ -internal class AutoCleanupManager( - private val application: Application, - private val cleanupCallback: () -> Unit, - private val backgroundTimeoutMs: Long = DEFAULT_BACKGROUND_TIMEOUT_MS, -) { - companion object { - // Default timeout after which we perform cleanup when app is in background (5 minutes) - const val DEFAULT_BACKGROUND_TIMEOUT_MS = 5 * 60 * 1000L - } - - private var isInBackground = false - private val cleanupHandler = Handler(Looper.getMainLooper()) - private var cleanupRunnable: Runnable? = null - - /** - * Start monitoring app lifecycle to perform automatic cleanup - */ - fun start() { - application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) - } - - /** - * Stop monitoring app lifecycle - */ - fun stop() { - application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) - cancelScheduledCleanup() - } - - /** - * Schedule cleanup after app has been in background for the specified time - */ - private fun scheduleCleanup() { - cancelScheduledCleanup() // Cancel any existing scheduled cleanup - - cleanupRunnable = - Runnable { - if (isInBackground) { - cleanupCallback.invoke() - } - } - - cleanupHandler.postDelayed(cleanupRunnable!!, backgroundTimeoutMs) - } - - /** - * Cancel any scheduled cleanup - */ - private fun cancelScheduledCleanup() { - cleanupRunnable?.let { - cleanupHandler.removeCallbacks(it) - } - } - - /** - * Activity lifecycle callbacks to detect app background state - */ - private val activityLifecycleCallbacks = - object : Application.ActivityLifecycleCallbacks { - private var activeActivities = 0 - - override fun onActivityCreated( - activity: Activity, - savedInstanceState: Bundle?, - ) {} - - override fun onActivityStarted(activity: Activity) { - if (activeActivities == 0) { - isInBackground = false - cancelScheduledCleanup() - } - activeActivities++ - } - - override fun onActivityResumed(activity: Activity) {} - - override fun onActivityPaused(activity: Activity) {} - - override fun onActivityStopped(activity: Activity) { - activeActivities-- - if (activeActivities == 0) { - // App is going to background - isInBackground = true - scheduleCleanup() - } - } - - override fun onActivitySaveInstanceState( - activity: Activity, - outState: Bundle, - ) {} - - override fun onActivityDestroyed(activity: Activity) {} - } -} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationStatusProvider.kt b/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationStatusProvider.kt deleted file mode 100644 index 2e522568..00000000 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationStatusProvider.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.pushpushgo.inappmessages.utils - -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager -import com.pushpushgo.inappmessages.model.UserAudienceType - -/** - * Utility class for accessing push notification subscription state - * from the PushPushGo SDK's SharedPreferences. - * - * This provides a bridge to the push notification SDK without creating a direct dependency. - */ -internal class PushNotificationStatusProvider( - private val context: Context, -) { - companion object { - // These constants match the ones in PushPushGo SDK's SharedPreferencesHelper - private const val IS_SUBSCRIBED = "_PushPushGoSDK_is_subscribed_" - private const val ARE_NOTIFICATIONS_BLOCKED = "_PushPushGoSDK_notifications_blocked_" - } - - private val sharedPreferences: SharedPreferences by lazy { - PreferenceManager.getDefaultSharedPreferences(context) - } - - /** - * Checks if the user is currently subscribed to push notifications - * by reading directly from the PushPushGo SDK's SharedPreferences - * - * @return true if subscribed (defaults to false if not found) - */ - fun isSubscribed(): Boolean { - val result = sharedPreferences.getBoolean(IS_SUBSCRIBED, false) - return result - } - - /** - * Checks if notifications are blocked for the current user - * - * @return true if notifications are blocked (defaults to false if not found) - */ - fun isNotificationsBlocked(): Boolean = sharedPreferences.getBoolean(ARE_NOTIFICATIONS_BLOCKED, false) - - /** - * Utility method to check if a user matches the given audience type - * - * @param audienceType The audience type to check against - * @return true if the current user matches the specified audience type - */ - fun matchesAudienceType(audienceType: UserAudienceType): Boolean = - when (audienceType) { - UserAudienceType.ALL -> true - UserAudienceType.SUBSCRIBER -> isSubscribed() && !isNotificationsBlocked() - UserAudienceType.NON_SUBSCRIBER -> !isSubscribed() || isNotificationsBlocked() - UserAudienceType.NOTIFICATIONS_BLOCKED -> isNotificationsBlocked() - } -} diff --git a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationSubscriber.kt b/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationSubscriber.kt deleted file mode 100644 index 64cb7db7..00000000 --- a/library-inappmessages/src/main/java/com/pushpushgo/inappmessages/utils/PushNotificationSubscriber.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.pushpushgo.inappmessages.utils - -import android.content.Context -import android.util.Log -import java.lang.reflect.InvocationTargetException - -/** - * Interface for requesting push notification subscription. - * This provides a clean way for the in-app messages to trigger a subscription request - * without directly depending on the push notification SDK. - */ -interface PushNotificationSubscriber { - /** - * Request user to subscribe to push notifications - * - * @param context Android context - * @return true if the subscription request was successfully initiated - */ - fun requestSubscription(context: Context): Boolean -} - -/** - * Default implementation that attempts to find the PushPushGoSubscriptionManager - * in the push notifications SDK using reflection. - * - * This provides automatic integration between the in-app messages library and - * the push notifications SDK without creating direct compile-time dependencies. - */ -internal class DefaultPushNotificationSubscriber : PushNotificationSubscriber { - companion object { - private const val TAG = "DefaultPushSubscriber" - private const val BRIDGE_CLASS = "com.pushpushgo.sdk.bridge.PushPushGoSubscriptionBridgeManager" - } - - override fun requestSubscription(context: Context): Boolean { - try { - // Try to find the PushPushGoSubscriptionManager class - val managerClass = Class.forName(BRIDGE_CLASS) - - // Create a new instance of the manager - val manager = managerClass.getDeclaredConstructor().newInstance() - - // Call the requestSubscription method - val method = managerClass.getMethod("requestSubscription", Context::class.java) - return method.invoke(manager, context) as Boolean - } catch (e: ClassNotFoundException) { - Log.e(TAG, "PushPushGoSubscriptionManager not found. Make sure the push notifications SDK is included", e) - } catch (e: InvocationTargetException) { - Log.e(TAG, "Error calling requestSubscription", e.targetException) - } catch (e: Exception) { - Log.e(TAG, "Error requesting subscription", e) - } - - Log.w(TAG, "Subscription requested but could not be processed automatically") - return false - } -} diff --git a/library-no-op/api/library-no-op.api b/library-no-op/api/library-no-op.api deleted file mode 100644 index d4bc9725..00000000 --- a/library-no-op/api/library-no-op.api +++ /dev/null @@ -1,101 +0,0 @@ -public final class com/pushpushgo/sdk/BeaconBuilder { - public final fun appendTag (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Lcom/pushpushgo/sdk/BeaconBuilder; - public static synthetic fun appendTag$default (Lcom/pushpushgo/sdk/BeaconBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun getTags ()Ljava/util/List; - public final fun getTagsToDelete ()Ljava/util/List; - public final fun removeTag ([Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun send ()V - public final fun set (Ljava/lang/String;Ljava/lang/Object;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun setCustomId (Ljava/lang/Integer;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun setCustomId (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; -} - -public final class com/pushpushgo/sdk/BuildConfig { - public static final field BUILD_TYPE Ljava/lang/String; - public static final field DEBUG Z - public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; - public fun ()V -} - -public final class com/pushpushgo/sdk/PushPushGo { - public static final field Companion Lcom/pushpushgo/sdk/PushPushGo$Companion; - public static final field VERSION Ljava/lang/String; - public synthetic fun (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun areNotificationsEnabled ()Z - public final fun createBeacon ()Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun createSubscriber ()Lcom/google/common/util/concurrent/ListenableFuture; - public final fun getApiKey ()Ljava/lang/String; - public final fun getDefaultIsSubscribed ()Z - public static final fun getInstance ()Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;)Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getNotificationDetails (Landroid/content/Intent;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public final fun getNotificationDetails (Ljava/util/Map;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public final fun getNotificationHandler ()Lkotlin/jvm/functions/Function2; - public final fun getOnInvalidProjectIdHandler ()Lkotlin/jvm/functions/Function3; - public final fun getProjectId ()Ljava/lang/String; - public final fun getPushToken ()Lcom/google/common/util/concurrent/ListenableFuture; - public final fun getSubscriberId ()Ljava/lang/String; - public final fun handleBackgroundNotificationClick (Landroid/content/Intent;)V - public final fun isPPGoPush (Landroid/content/Intent;)Z - public final fun isPPGoPush (Ljava/util/Map;)Z - public final fun isSubscribed ()Z - public final fun migrateToNewProject (Ljava/lang/String;Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; - public final fun registerSubscriber ()V - public final fun setDefaultIsSubscribed (Z)V - public final fun setNotificationHandler (Lkotlin/jvm/functions/Function2;)V - public final fun setOnInvalidProjectIdHandler (Lkotlin/jvm/functions/Function3;)V - public final fun unregisterSubscriber ()V - public final fun unregisterSubscriber (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; -} - -public final class com/pushpushgo/sdk/PushPushGo$Companion { - public final fun getInstance ()Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public static synthetic fun getInstance$default (Lcom/pushpushgo/sdk/PushPushGo$Companion;Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun isInitialized ()Z -} - -public final class com/pushpushgo/sdk/dto/PPGoNotification { - public fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()I - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public static synthetic fun copy$default (Lcom/pushpushgo/sdk/dto/PPGoNotification;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public fun equals (Ljava/lang/Object;)Z - public final fun getBody ()Ljava/lang/String; - public final fun getCampaignId ()Ljava/lang/String; - public final fun getPriority ()I - public final fun getRedirectLink ()Ljava/lang/String; - public final fun getTitle ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/pushpushgo/sdk/exception/PushPushException : java/io/IOException { -} - -public final class com/pushpushgo/sdk/push/service/FcmMessagingServiceDelegate { - public fun (Landroid/content/Context;)V - public final fun onDestroy ()V - public final fun onMessageReceived (Lcom/google/firebase/messaging/RemoteMessage;)V - public final fun onNewToken (Ljava/lang/String;)V -} - -public final class com/pushpushgo/sdk/push/service/HmsMessagingServiceDelegate { - public fun (Landroid/content/Context;)V - public final fun onDestroy ()V - public final fun onMessageReceived (Lcom/huawei/hms/push/RemoteMessage;)V - public final fun onNewToken (Ljava/lang/String;)V -} - diff --git a/library-no-op/build.gradle b/library-no-op/build.gradle deleted file mode 100644 index fa9ae3ac..00000000 --- a/library-no-op/build.gradle +++ /dev/null @@ -1,72 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'maven-publish' - alias(libs.plugins.validator) -} - -android { - compileSdk 36 - - namespace 'com.pushpushgo.sdk' - defaultConfig { - minSdkVersion 21 - targetSdkVersion 36 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - buildFeatures { - buildConfig true - } - compileOptions { - sourceCompatibility JavaLanguageVersion.of(17) - targetCompatibility JavaLanguageVersion.of(17) - } - - kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget("17") - } - } - - publishing { - singleVariant("release") { - withSourcesJar() - } - } -} - -dependencies { - compileOnly libs.hms.push - - compileOnly platform(libs.firebase.bom) - compileOnly 'com.google.firebase:firebase-messaging' - - api "com.google.guava:guava:33.4.8-android" -} - -tasks.register('androidSourcesJar', Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -publishing { - publications { - release(MavenPublication) { - groupId = 'com.pushpushgo' - artifactId = 'sdk-no-op' - version = libs.versions.sdk.get() - - afterEvaluate { - from components.release - } - } - } -} diff --git a/library-no-op/src/main/AndroidManifest.xml b/library-no-op/src/main/AndroidManifest.xml deleted file mode 100644 index cc947c56..00000000 --- a/library-no-op/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/BeaconBuilder.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/BeaconBuilder.kt deleted file mode 100644 index 4271dc0e..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/BeaconBuilder.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.pushpushgo.sdk - -@Suppress("unused", "UNUSED_PARAMETER") -class BeaconBuilder internal constructor() { - @JvmOverloads - fun appendTag(tag: String, label: String = "default", strategy: String = "append", ttl: Int = 0): BeaconBuilder = this - fun getTags(): MutableList> = mutableListOf() - fun getTagsToDelete(): MutableList = mutableListOf() - fun removeTag(vararg name: String): BeaconBuilder = this - fun send() = Unit - fun set(key: String, value: Any): BeaconBuilder = this - fun setCustomId(id: Int?): BeaconBuilder = this - fun setCustomId(id: String?): BeaconBuilder = this -} diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/PushPushGo.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/PushPushGo.kt deleted file mode 100644 index 511c955b..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/PushPushGo.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.pushpushgo.sdk - -import android.app.Application -import android.content.Context -import android.content.Intent -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.pushpushgo.sdk.BuildConfig.DEBUG -import com.pushpushgo.sdk.dto.PPGoNotification -import com.pushpushgo.sdk.exception.PushPushException - -@Suppress("unused", "UNUSED_PARAMETER") -class PushPushGo private constructor( - private val application: Application, - private val apiKey: String, - private val projectId: String, - private val isProduction: Boolean, - private val isNetworkDebug: Boolean, - private val customBaseUrl: String? -) { - - companion object { - const val VERSION = "NO-OP" - - private var INSTANCE: PushPushGo? = null - - fun isInitialized(): Boolean = INSTANCE != null - - @JvmStatic - fun getInstance(): PushPushGo = - INSTANCE ?: throw PushPushException("You have to initialize PushPushGo with context first!") - - @JvmStatic - fun getInstance(application: Application) = INSTANCE ?: synchronized(this) { - INSTANCE ?: PushPushGo(application, "", "", true, DEBUG, null).also { INSTANCE = it } - } - - @JvmStatic - @JvmOverloads - fun getInstance( - application: Application, apiKey: String, projectId: String, isProduction: Boolean, isDebug: Boolean = false, customBaseUrl: String?, - ): PushPushGo { - if (INSTANCE == null) { - INSTANCE = PushPushGo(application, apiKey, projectId, isProduction, isDebug, customBaseUrl) - } - return INSTANCE as PushPushGo - } - } - - var defaultIsSubscribed: Boolean = false - - var notificationHandler: NotificationHandler = { _, _ -> } - - var onInvalidProjectIdHandler: InvalidProjectIdHandler = { _, _, _ -> } - - fun createBeacon(): BeaconBuilder = BeaconBuilder() - - fun getApiKey(): String = apiKey - - fun getNotificationDetails(notificationIntent: Intent?): PPGoNotification? = null - - fun getNotificationDetails(notificationData: Map): PPGoNotification? = null - - fun getProjectId(): String = projectId - - fun getSubscriberId(): String = "" - - fun getPushToken(): ListenableFuture = Futures.immediateFuture("null") - - fun handleBackgroundNotificationClick(intent: Intent?) = Unit - - fun isPPGoPush(notificationIntent: Intent?): Boolean = false - - fun isPPGoPush(notificationData: Map): Boolean = false - - fun isSubscribed(): Boolean = false - - fun migrateToNewProject(newProjectId: String, newProjectToken: String): ListenableFuture = - Futures.immediateFuture(this) - - fun areNotificationsEnabled(): Boolean = false - - fun registerSubscriber() = Unit - - fun createSubscriber(): ListenableFuture = Futures.immediateFuture("") - - fun unregisterSubscriber() = Unit - - fun unregisterSubscriber(projectId: String, projectToken: String, subscriberId: String): ListenableFuture = - Futures.immediateFuture(Unit) -} - -typealias NotificationHandler = (context: Context, url: String) -> Unit - -typealias InvalidProjectIdHandler = (pushProjectId: String, pushSubscriberId: String, currentProjectId: String) -> Unit diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt deleted file mode 100644 index ca299871..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.pushpushgo.sdk.dto - -data class PPGoNotification( - val title: String?, - val body: String?, - val priority: Int = 0, - val campaignId: String, - val redirectLink: String?, -) diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/exception/PushPushException.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/exception/PushPushException.kt deleted file mode 100644 index 29461f4b..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/exception/PushPushException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.pushpushgo.sdk.exception - -import java.io.IOException - -class PushPushException internal constructor(message: String) : IOException(message) diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/FcmMessagingServiceDelegate.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/FcmMessagingServiceDelegate.kt deleted file mode 100644 index da613d91..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/FcmMessagingServiceDelegate.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.pushpushgo.sdk.push.service - -import android.content.Context -import com.google.firebase.messaging.RemoteMessage - -@Suppress("unused", "UNUSED_PARAMETER") -class FcmMessagingServiceDelegate(private val context: Context) { - - fun onDestroy() = Unit - - fun onMessageReceived(remoteMessage: RemoteMessage) = Unit - - fun onNewToken(token: String) = Unit -} diff --git a/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/HmsMessagingServiceDelegate.kt b/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/HmsMessagingServiceDelegate.kt deleted file mode 100644 index 6691ca50..00000000 --- a/library-no-op/src/main/java/com/pushpushgo/sdk/push/service/HmsMessagingServiceDelegate.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.pushpushgo.sdk.push.service - -import android.content.Context -import com.huawei.hms.push.RemoteMessage - -@Suppress("unused", "UNUSED_PARAMETER") -class HmsMessagingServiceDelegate(private val context: Context) { - - fun onDestroy() = Unit - - fun onMessageReceived(remoteMessage: RemoteMessage) = Unit - - fun onNewToken(token: String) = Unit -} diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index 92c132b7..00000000 --- a/library/build.gradle +++ /dev/null @@ -1,119 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinVersion - -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'maven-publish' - id('com.google.devtools.ksp').version('2.2.0-2.0.2') - alias(libs.plugins.dokka) - alias(libs.plugins.validator) - alias(libs.plugins.ktlint) -} - -apply from: 'jacoco.gradle' - -tasks.named("dokkaHtml") { - moduleName.set("PushPushGo SDK") -} - -android { - namespace 'com.pushpushgo.sdk' - compileSdk 36 - - defaultConfig { - minSdk 23 - targetSdk 36 - - consumerProguardFiles("ppgo-sdk.pro") - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - } - } - buildFeatures { - buildConfig true - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlin { - compilerOptions { - jvmTarget = JvmTarget.fromTarget("17") - languageVersion = KotlinVersion.fromVersion("2.1") - apiVersion = KotlinVersion.fromVersion("2.1") - } - } - - testOptions { - unitTests { - includeAndroidResources = true - } - } - publishing { - singleVariant("release") { - withSourcesJar() - } - } -} - -dependencies { - implementation libs.androidx.core.ktx - implementation libs.androidx.preference - - implementation libs.kodein - - implementation libs.coroutines.core - implementation libs.coroutines.android - implementation libs.coroutines.guava - - implementation libs.retrofit - implementation libs.retrofit.moshi - implementation platform(libs.okhttp.bom) - implementation 'com.squareup.okhttp3:logging-interceptor' - - implementation libs.moshi.kotlin - ksp libs.moshi.codegen - implementation libs.moshi.adapters - - implementation libs.androidx.work.runtime - implementation libs.androidx.work.gcm - - compileOnly platform(libs.firebase.bom) - compileOnly 'com.google.firebase:firebase-messaging' - - compileOnly libs.hms.push - - // testing - testImplementation 'junit:junit:4.13.2' - testImplementation 'io.mockk:mockk:1.14.5' - testImplementation 'org.json:json:20250517' - testImplementation 'androidx.test.ext:junit-ktx:1.3.0' - testImplementation 'org.robolectric:robolectric:4.15.1' - - testImplementation platform(libs.firebase.bom) - testImplementation 'com.google.firebase:firebase-messaging' -} - -tasks.register('androidSourcesJar', Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -publishing { - publications { - release(MavenPublication) { - groupId = 'com.pushpushgo' - artifactId = 'sdk' - version = libs.versions.sdk.get() - - afterEvaluate { - from components.release - } - } - } -} diff --git a/library/jacoco.gradle b/library/jacoco.gradle deleted file mode 100644 index 2e76184f..00000000 --- a/library/jacoco.gradle +++ /dev/null @@ -1,40 +0,0 @@ -apply plugin: "jacoco" - -jacoco { - toolVersion "0.8.11" - reportsDirectory.set(layout.buildDirectory.dir('reports')) -} - -tasks.register('jacocoTestReport', JacocoReport) { - group = "Reporting" - description = "Generate Jacoco coverage reports" - - reports { - xml.required.set(true) - html.required.set(true) - } - - def excludes = [ - '**/R.class', - '**/R$*.class', - '**/BuildConfig.*', - '**/*Test*.*', - '**/*JsonAdapter.*' - ] - - classDirectories.setFrom(fileTree( - dir: "$buildDir/intermediates/classes/debug", - excludes: excludes - ) + fileTree( - dir: "$buildDir/tmp/kotlin-classes/debug", - excludes: excludes - )) - - sourceDirectories.setFrom(files([ - "src/main/java" - ])) - executionData.setFrom(fileTree( - dir: project.projectDir, - includes: ["**/*.exec", "**/*.ec"] - )) -} diff --git a/library/ppgo-sdk.pro b/library/ppgo-sdk.pro deleted file mode 100644 index 974027eb..00000000 --- a/library/ppgo-sdk.pro +++ /dev/null @@ -1,27 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile - --keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference --keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest - --keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference --keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest diff --git a/library/src/main/java/com/pushpushgo/sdk/NotificationStatusChecker.kt b/library/src/main/java/com/pushpushgo/sdk/NotificationStatusChecker.kt deleted file mode 100644 index 1fc1cc83..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/NotificationStatusChecker.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.pushpushgo.sdk - -import android.app.ActivityManager -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -import android.content.Context -import android.os.Build -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.NotificationManagerCompat.IMPORTANCE_NONE -import androidx.core.content.getSystemService -import com.pushpushgo.sdk.network.SharedPreferencesHelper -import com.pushpushgo.sdk.utils.logDebug -import com.pushpushgo.sdk.utils.logError -import kotlinx.coroutines.* -import java.util.* - -internal class NotificationStatusChecker private constructor( - private val context: Context, -) : TimerTask() { - private val checkerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - private val errorHandler = CoroutineExceptionHandler { _, e -> logError(e) } - - private val pref = SharedPreferencesHelper(context) - - private val notificationManager = NotificationManagerCompat.from(context) - - private val activityManager = context.getSystemService() - - companion object { - private const val CHECK_PERIOD = 10_000L - - fun start(context: Context) { - Timer().scheduleAtFixedRate(NotificationStatusChecker(context), 0, CHECK_PERIOD) - } - } - - override fun run() { - if (isAppOnForeground()) checkNotificationsStatus() - } - - private fun isAppOnForeground(): Boolean = - activityManager?.runningAppProcesses.orEmpty().any { - it.importance == IMPORTANCE_FOREGROUND && it.processName == context.packageName - } - - private fun checkNotificationsStatus() { - if (areNotificationsEnabled() && pref.isSubscribed) { - if (BuildConfig.DEBUG) logDebug("Notifications enabled") - - if (pref.subscriberId.isBlank()) { - checkerScope.launch(errorHandler) { - logDebug("Notifications enabled, but not subscribed. Registering token...") - PushPushGo.getInstance().registerSubscriber() - } - } - } else { - if (BuildConfig.DEBUG) logDebug("Notifications disabled") - - if (pref.subscriberId.isNotBlank()) { - checkerScope.launch(errorHandler) { - logDebug("Notifications disabled, but subscribed. Unregistering subscriber...") - PushPushGo.getInstance().getNetwork().unregisterSubscriber() - } - } - } - } - - private fun areNotificationsEnabled(): Boolean { - val areNotificationsEnabled = notificationManager.areNotificationsEnabled() - pref.areNotificationsBlocked = !areNotificationsEnabled - if (!areNotificationsEnabled) return false - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true - - val channelName = context.getString(R.string.pushpushgo_notification_default_channel_id) - val channel = notificationManager.getNotificationChannel(channelName) - - return channel?.importance != IMPORTANCE_NONE - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/PushPushGo.kt b/library/src/main/java/com/pushpushgo/sdk/PushPushGo.kt deleted file mode 100644 index d665fa09..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/PushPushGo.kt +++ /dev/null @@ -1,604 +0,0 @@ -package com.pushpushgo.sdk - -import android.app.Application -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import androidx.core.app.NotificationManagerCompat -import com.google.common.util.concurrent.ListenableFuture -import com.pushpushgo.sdk.BuildConfig.DEBUG -import com.pushpushgo.sdk.data.EventType -import com.pushpushgo.sdk.data.mapToDto -import com.pushpushgo.sdk.di.NetworkModule -import com.pushpushgo.sdk.di.WorkModule -import com.pushpushgo.sdk.dto.PPGoNotification -import com.pushpushgo.sdk.exception.PushPushException -import com.pushpushgo.sdk.push.PushNotificationDelegate -import com.pushpushgo.sdk.push.areNotificationsEnabled -import com.pushpushgo.sdk.push.createNotificationChannel -import com.pushpushgo.sdk.push.deserializeNotificationData -import com.pushpushgo.sdk.push.handleNotificationLinkClick -import com.pushpushgo.sdk.push.liveactivity.LiveActivityHandler -import com.pushpushgo.sdk.push.liveactivity.LiveActivityManager -import com.pushpushgo.sdk.push.liveactivity.LiveActivityPersistence -import com.pushpushgo.sdk.push.liveactivity.data.LiveActivity -import com.pushpushgo.sdk.push.liveactivity.data.LiveActivityPayloadParser -import com.pushpushgo.sdk.utils.getPlatformPushToken -import com.pushpushgo.sdk.utils.getPlatformType -import com.pushpushgo.sdk.utils.logDebug -import com.pushpushgo.sdk.utils.logError -import com.pushpushgo.sdk.utils.mapToBundle -import com.pushpushgo.sdk.utils.validateApiKey -import com.pushpushgo.sdk.utils.validateProjectId -import com.pushpushgo.sdk.work.UploadDelegate -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.guava.future -import kotlinx.coroutines.launch - -class PushPushGo private constructor( - private val application: Application, - private val apiKey: String, - private val projectId: String, - private val isProduction: Boolean, - internal val isDebug: Boolean, - private val customBaseUrl: String?, -) { - companion object { - const val VERSION = "3.2.0" - - internal const val TAG = "PPGo" - - /** - * an instance of PushPushGo library - */ - @Volatile - private var INSTANCE: PushPushGo? = null - - fun isInitialized(): Boolean = INSTANCE != null - - @JvmStatic - fun getInstance(): PushPushGo = INSTANCE ?: throw PushPushException("You have to initialize PushPushGo with context first!") - - /** - * function to create an instance of PushPushGo object to handle push notifications - * @param application - an application to get apiKey from META DATA stored in Your Manifest.xml file - * @return PushPushGo instance - */ - @JvmStatic - fun getInstance(application: Application) = - INSTANCE ?: synchronized(this) { - INSTANCE ?: buildPushPushGoFromContext(application).also { INSTANCE = it } - } - - /** - * function to create an instance of PushPushGo object to handle push notifications - * @param application - an application to handle DI - * @param apiKey - key to communicate with RESTFul API - * @param projectId - project identifier - * @param isProduction - flag indicating if this is a production environment - * @param isDebug - flag for debug mode - * @param customBaseUrl - optional custom base URL for API endpoints - * @return PushPushGo instance - */ - @JvmStatic - @JvmOverloads - fun getInstance( - application: Application, - apiKey: String, - projectId: String, - isProduction: Boolean, - isDebug: Boolean = DEBUG, - customBaseUrl: String? = null, - ): PushPushGo { - if (INSTANCE == null) { - INSTANCE = createPushPushGoInstance(application, apiKey, projectId, isProduction, isDebug, customBaseUrl) - } - return INSTANCE as PushPushGo - } - - @JvmStatic - private fun reinitialize( - application: Application, - apiKey: String, - projectId: String, - isProduction: Boolean, - isDebug: Boolean, - customBaseUrl: String?, - ): PushPushGo { - INSTANCE = createPushPushGoInstance(application, apiKey, projectId, isProduction, isDebug, customBaseUrl) - - return INSTANCE as PushPushGo - } - - private fun buildPushPushGoFromContext(application: Application): PushPushGo { - val (projectId, apiKey) = extractCredentialsFromContext(application) - - return createPushPushGoInstance(application, apiKey, projectId, isProduction = true, DEBUG, null) - } - - private fun extractCredentialsFromContext(context: Context): Pair { - val ai = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.packageManager.getApplicationInfo( - context.packageName, - PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()), - ) - } else { - context.packageManager.getApplicationInfo( - context.packageName, - PackageManager.GET_META_DATA, - ) - } - - val bundle = ai.metaData - val apiKey = - bundle.getString("com.pushpushgo.apikey") - ?: throw PushPushException("You have to declare apiKey in Your Manifest file") - val projectId = - bundle.getString("com.pushpushgo.projectId") - ?: throw PushPushException("You have to declare projectId in Your Manifest file") - - return projectId to apiKey - } - - private fun createPushPushGoInstance( - app: Application, - key: String, - project: String, - isProduction: Boolean, - isDebug: Boolean, - customBaseUrl: String?, - ): PushPushGo { - validateCredentials(project, key) - return PushPushGo(app, key, project, isProduction, isDebug, customBaseUrl) - } - - private fun validateCredentials( - projectId: String, - apiKey: String, - ) { - validateApiKey(apiKey) - validateProjectId(projectId) - } - } - - init { - val platformType = getPlatformType() - val startupMessage = "PushPushGo $VERSION initialized (project id: $projectId, platform: $platformType)" - println(startupMessage) - - createNotificationChannel(application) - NotificationStatusChecker.start(application) - } - - private val networkModule by lazy { - NetworkModule( - context = application, - apiKey = apiKey, - projectId = projectId, - isProduction = isProduction, - isDebug = isDebug, - customBaseUrl = customBaseUrl, - ) - } - - private val workModule by lazy { WorkModule(application) } - - private val uploadDelegate by lazy { UploadDelegate() } - - private val sdkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val liveActivityPersistence: LiveActivityPersistence? by lazy { - if (Build.VERSION.SDK_INT >= 36) LiveActivityPersistence(application) else null - } - - private val liveActivityManager: LiveActivityManager? by lazy { - liveActivityPersistence?.let { LiveActivityManager(it) } - } - - internal val liveActivityHandler: LiveActivityHandler? by lazy { - val manager = liveActivityManager ?: return@lazy null - LiveActivityHandler( - context = application, - scope = sdkScope, - manager = manager, - apiRepository = getNetwork(), - onEvent = { eventType, laId, _, _, liveDataVersion -> - // Map internal LA lifecycle events to the backend statistics enum - // (started / closed / clicked / clicked_1 / clicked_2) and report them - // to the dedicated live-notification events endpoint. - val statisticsType = - when (eventType) { - "la.started" -> "started" - "la.clicked" -> "clicked" - "la.clicked_1" -> "clicked_1" - "la.clicked_2" -> "clicked_2" - "la.dismissed" -> "closed" - else -> null // la.ended not reported (no backend enum value) - } - if (statisticsType != null) { - sdkScope.launch { - runCatching { - getNetwork().sendLiveActivityEvent( - liveNotificationId = laId, - eventType = statisticsType, - liveDataVersion = liveDataVersion, - subscriberId = getSubscriberId(), - ) - }.onFailure { logError("Failed to send LA statistics event $statisticsType for $laId", it) } - } - } - }, - ) - } - - init { - if (Build.VERSION.SDK_INT >= 36) { - liveActivityManager?.restoreFromPersistence() - } - } - - internal fun getNetwork() = networkModule.apiRepository - - internal fun getUploadManager() = workModule.uploadManager - - var onInvalidProjectIdHandler: InvalidProjectIdHandler = { pushProjectId, _, currentProjectId -> - logDebug("Project ID inconsistency detected! Project ID from push is $pushProjectId while SDK is configured with $currentProjectId") - } - - /** - * Settings used for migration to support switch user before start first time app after upgrade/switch - * defaultIsSubscribed default is false - */ - var defaultIsSubscribed: Boolean = false - - var notificationHandler: NotificationHandler = { context, url, overrideFlags -> - handleNotificationLinkClick( - context, - url, - overrideFlags, - ) - } - - /** - * function to check whether the given notification data belongs to the PPGo sender - * - * @param notificationIntent - pending intent of clicked notification - * - * @return boolean - */ - fun isPPGoPush(notificationIntent: Intent?): Boolean = notificationIntent?.hasExtra("project") == true - - /** - * function to check whether the given notification data belongs to the PPGo sender - * - * @param notificationData - data field of received notification - * - * @return boolean - */ - fun isPPGoPush(notificationData: Map): Boolean = notificationData.containsKey("project") - - /** - * function to retrieve PPGo notification details - * - * @param notificationIntent - pending intent of clicked notification - * - * @return Notification - */ - fun getNotificationDetails(notificationIntent: Intent?): PPGoNotification? = - deserializeNotificationData(notificationIntent?.extras)?.mapToDto() - - /** - * function to retrieve PPGo notification details - * - * @param notificationData - data field of received notification - * - * @return Notification - */ - fun getNotificationDetails(notificationData: Map): PPGoNotification? = - deserializeNotificationData(notificationData.mapToBundle())?.mapToDto() - - /** - * function set custom intent flags to shared preferences - * when push receives then check for this flags and add - * them in PendingIntent launcherActivity as flags - */ - fun setCustomClickIntentFlags(flags: Int) { - networkModule.sharedPref.customIntentFlags = flags - } - - /** - * function returns custom flags from shared contexts for click intent - */ - fun getCustomClickIntentFlags(): Int = networkModule.sharedPref.customIntentFlags - - /** - * helper function to handle click on notification from background - */ - fun handleBackgroundNotificationClick( - intent: Intent?, - overrideFlags: Int = Intent.FLAG_ACTIVITY_NEW_TASK, - ) { - if (intent?.hasExtra(PushNotificationDelegate.PROJECT_ID_EXTRA) != true) return - - val intentProjectId = intent.getStringExtra(PushNotificationDelegate.PROJECT_ID_EXTRA) - val intentSubscriberId = intent.getStringExtra(PushNotificationDelegate.SUBSCRIBER_ID_EXTRA).orEmpty() - val intentButtonId = intent.getIntExtra(PushNotificationDelegate.BUTTON_ID_EXTRA, 0) - val intentLink = intent.getStringExtra(PushNotificationDelegate.LINK_EXTRA).orEmpty() - val intentCampaignId = intent.getStringExtra(PushNotificationDelegate.CAMPAIGN_ID_EXTRA).orEmpty() - val intentNotificationId = intent.getIntExtra(PushNotificationDelegate.NOTIFICATION_ID_EXTRA, 0) - - if (intentProjectId != getInstance().projectId) { - return onInvalidProjectIdHandler(intentProjectId.orEmpty(), intentSubscriberId, getInstance().projectId) - } - - NotificationManagerCompat.from(application).cancel(intentNotificationId) - - // TODO Remove duplicated code - val notify = deserializeNotificationData(intent.extras) - notificationHandler(application, notify?.redirectLink ?: intentLink, overrideFlags) - intent.removeExtra(PushNotificationDelegate.PROJECT_ID_EXTRA) - - uploadDelegate.sendEvent( - type = EventType.CLICKED, - buttonId = intentButtonId, - projectId = notify?.project ?: intentProjectId, - subscriberId = notify?.subscriber ?: intentSubscriberId, - campaign = notify?.campaignId ?: intentCampaignId, - ) - } - - /** - * function to read Your API Key from an PushPushGo library instance - * @return API Key String - */ - fun getApiKey(): String = apiKey - - /** - * function to read Your API Key from an PushPushGo library instance - * @return API Key String - */ - fun getProjectId(): String = projectId - - /** - * function to read Your subscriber id from an PushPushGo library instance - * @return subscriber id String - */ - fun getSubscriberId(): String = networkModule.sharedPref.subscriberId - - /** - * function to check if user subscribed to notifications - * @return boolean true if subscribed - */ - fun isSubscribed(): Boolean = networkModule.sharedPref.isSubscribed - - /** - * function to retrieve last push token used to subscribe that - */ - fun getPushToken(): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - networkModule.sharedPref.lastToken.takeIf { it.isNotEmpty() } ?: getPlatformPushToken(application) - } - - /** - * function to register subscriber - */ - fun registerSubscriber() { - if (!areNotificationsEnabled()) { - return logError("Notifications disabled! Subscriber registration canceled") - } - - networkModule.sharedPref.isSubscribed = true - getUploadManager().sendRegister(null) - } - - /** - * function to register subscriber and returns future with subscriber id - * - * @return string subscriber ID - */ - fun createSubscriber(): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - check(areNotificationsEnabled()) { - "Notifications disabled! Subscriber registration canceled" - } - - getNetwork().registerToken(null) - networkModule.sharedPref.isSubscribed = true - getSubscriberId() - } - - /** - * function to unregister subscriber - */ - fun unregisterSubscriber() { - getUploadManager().sendUnregister() - networkModule.sharedPref.isSubscribed = false - } - - fun unregisterSubscriber( - projectId: String, - projectToken: String, - subscriberId: String, - ): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - getInstance().getNetwork().unregisterSubscriber( - projectId = projectId, - token = projectToken, - subscriberId = subscriberId, - ) - networkModule.sharedPref.isSubscribed = false - } - - /** - * function to re-subscribe to different project (previously unsubscribe from current project) - * WARNING: after resubscribe use object returned by this function instead of previous one - * - * @param newProjectId - project id to which we are switching - * @param newProjectToken - project token - */ - fun migrateToNewProject( - newProjectId: String, - newProjectToken: String, - ): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - check(areNotificationsEnabled()) { - "Notifications disabled! Subscriber registration canceled" - } - - getInstance().getNetwork().migrateSubscriber( - newProjectId = newProjectId, - newToken = newProjectToken, - ) - reinitialize( - application = application, - projectId = newProjectId, - apiKey = newProjectToken, - isProduction = isProduction, - isDebug = isDebug, - customBaseUrl = customBaseUrl, - ).apply { - notificationHandler = this@PushPushGo.notificationHandler - onInvalidProjectIdHandler = this@PushPushGo.onInvalidProjectIdHandler - } - } - - fun areNotificationsEnabled(): Boolean { - val areEnabled = areNotificationsEnabled(application) - networkModule.sharedPref.areNotificationsBlocked = !areEnabled - return areEnabled - } - - /** - * function to construct and send beacon - */ - fun createBeacon(): BeaconBuilder = BeaconBuilder(uploadDelegate) - - /** - * Checks whether Live Activities are supported on this device. - * Requires API 36+ (Android 16) for ProgressStyle notifications. - */ - fun isLiveActivitiesSupported(): Boolean = Build.VERSION.SDK_INT >= 36 - - /** - * Returns the list of currently active live activities. - * Returns empty list on API < 36. - */ - fun getActiveLiveActivities(): List = liveActivityManager?.getActiveActivities() ?: emptyList() - - /** - * Checks whether a specific live activity is currently active. - * Returns false on API < 36. - */ - fun isLiveActivityActive(id: String): Boolean = liveActivityManager?.isActivityActive(id) ?: false - - /** - * Simulates a Live Activity push for testing purposes. - * No-op on API < 36. - * - * Pass a data map matching the Live Activity push payload format. - */ - fun simulateLiveActivityPush(data: Map) { - liveActivityHandler?.handlePush(data) - } - - /** - * Subscribes this device to a backend live notification (Live Activity) so it - * starts receiving its push updates. - * - * The device must already be a registered push subscriber (call - * [createSubscriber] first). The returned future resolves to the backend LA - * subscriber id, which is also persisted so [unsubscribeFromLiveActivity] can - * be called later without tracking it yourself. - * - * @param liveNotificationId backend id of the live notification to follow. - * @return future with the assigned LA subscriber id. - */ - fun subscribeToLiveActivity(liveNotificationId: String): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - val laSubscriberId = getNetwork().subscribeToLiveActivity(liveNotificationId) - networkModule.sharedPref.setLiveActivitySubscriberId(liveNotificationId, laSubscriberId) - // Catch up: render the current state for subscribers that joined after the - // `start` push was already delivered (no-op if the LA isn't live yet). - catchUpLiveActivity(liveNotificationId) - laSubscriberId - } - - /** - * Fetch the current live notification state and feed it through the render - * pipeline as a synthetic `start`, so a late subscriber sees the running - * activity without waiting for the next update push. - */ - private suspend fun catchUpLiveActivity(liveNotificationId: String) { - val handler = liveActivityHandler ?: return - val json = getNetwork().fetchLiveActivity(liveNotificationId) ?: return - val envelope = LiveActivityPayloadParser.buildCatchUpEnvelope(json) ?: return - handler.handlePush(envelope) - } - - /** - * Unsubscribes this device from a backend live notification it previously - * subscribed to via [subscribeToLiveActivity]. No-op if the device is not - * subscribed to it. - * - * @param liveNotificationId backend id of the live notification to leave. - */ - fun unsubscribeFromLiveActivity(liveNotificationId: String): ListenableFuture = - CoroutineScope(Job() + Dispatchers.IO).future { - val laSubscriberId = networkModule.sharedPref.getLiveActivitySubscriberId(liveNotificationId) - check(laSubscriberId.isNotEmpty()) { - "Not subscribed to live notification $liveNotificationId" - } - getNetwork().unsubscribeFromLiveActivity(liveNotificationId, laSubscriberId) - networkModule.sharedPref.removeLiveActivitySubscriberId(liveNotificationId) - } - - /** - * Returns the persisted LA subscriber id for a live notification, or empty - * string if this device is not subscribed to it. - */ - fun getLiveActivitySubscriberId(liveNotificationId: String): String = - networkModule.sharedPref.getLiveActivitySubscriberId(liveNotificationId) - - /** - * Handles a Live Activity notification click from the background. - * - * Call this from Activity.onCreate() and Activity.onNewIntent() - * alongside [handleBackgroundNotificationClick]. - * - * When the click carries a deep link it is opened through [notificationHandler] - * (the same routing used for regular push clicks) unless [openDeepLink] is - * false — pass false to handle the returned link yourself. - * - * @param intent The intent received by the activity. - * @param openDeepLink Whether the SDK should open the deep link itself. - * @return The deep link string if this was a Live Activity click, null otherwise. - */ - @JvmOverloads - fun handleLiveActivityClick( - intent: Intent?, - openDeepLink: Boolean = true, - ): String? { - val laId = intent?.getStringExtra(LiveActivityHandler.EXTRA_LIVE_ACTIVITY_ID) ?: return null - val deepLink = intent.getStringExtra(LiveActivityHandler.EXTRA_DEEP_LINK) - val actionIndex = intent.getIntExtra(LiveActivityHandler.EXTRA_ACTION_INDEX, -1) - - liveActivityHandler?.handleClick(laId, actionIndex) - intent.removeExtra(LiveActivityHandler.EXTRA_LIVE_ACTIVITY_ID) - intent.removeExtra(LiveActivityHandler.EXTRA_ACTION_INDEX) - - if (openDeepLink && !deepLink.isNullOrBlank()) { - notificationHandler(application, deepLink, Intent.FLAG_ACTIVITY_NEW_TASK) - } - - return deepLink - } -} - -typealias NotificationHandler = (context: Context, url: String, overrideFlags: Int) -> Unit - -typealias InvalidProjectIdHandler = (pushProjectId: String, pushSubscriberId: String, currentProjectId: String) -> Unit diff --git a/library/src/main/java/com/pushpushgo/sdk/bridge/PushSubscriptionManager.kt b/library/src/main/java/com/pushpushgo/sdk/bridge/PushSubscriptionManager.kt deleted file mode 100644 index 4aba394d..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/bridge/PushSubscriptionManager.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.pushpushgo.sdk.bridge - -import android.Manifest -import android.app.Activity -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import android.util.Log -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.pushpushgo.sdk.PushPushGo -import com.pushpushgo.sdk.push.areNotificationsEnabled - -/** - * Bridge interface that allows other components (in-app messages library) - * to request push notification subscription without creating direct dependencies. - */ -interface PushSubscriptionBridgeManager { - /** - * Request user to subscribe to push notifications - * - * @param context Android context - * @return true if the subscription request was successfully initiated - */ - fun requestSubscription(context: Context): Boolean -} - -class PushPushGoSubscriptionBridgeManager : PushSubscriptionBridgeManager { - companion object { - private const val TAG = "PushSubscriptionManager" - private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 1001 - } - - override fun requestSubscription(context: Context): Boolean { - try { - if (!PushPushGo.isInitialized()) { - Log.e(TAG, "PushPushGo SDK is not initialized") - return false - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) - != PackageManager.PERMISSION_GRANTED - ) { - if (context is Activity) { - Log.d(TAG, "Requesting POST_NOTIFICATIONS permission") - ActivityCompat.requestPermissions( - context, - arrayOf(Manifest.permission.POST_NOTIFICATIONS), - NOTIFICATION_PERMISSION_REQUEST_CODE, - ) - } else { - Log.w(TAG, "Cannot request permission: context is not an Activity") - return false - } - } - } - - val pushSdk = PushPushGo.getInstance() - Log.d(TAG, "Requesting push notification subscription") - pushSdk.registerSubscriber() - - val notificationsEnabled = areNotificationsEnabled(context) - Log.d(TAG, "Notifications enabled after registration: $notificationsEnabled") - - return notificationsEnabled - } catch (e: Exception) { - Log.e(TAG, "Error requesting subscription", e) - return false - } - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/data/BeaconTag.kt b/library/src/main/java/com/pushpushgo/sdk/data/BeaconTag.kt deleted file mode 100644 index 03235796..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/data/BeaconTag.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.pushpushgo.sdk.data - -internal data class BeaconTag( - val tag: String, - val label: String, - val strategy: String = "append", - val ttl: Int = 0, -) diff --git a/library/src/main/java/com/pushpushgo/sdk/data/NotificationMapper.kt b/library/src/main/java/com/pushpushgo/sdk/data/NotificationMapper.kt deleted file mode 100644 index f9f5e499..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/data/NotificationMapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.pushpushgo.sdk.data - -import com.pushpushgo.sdk.dto.PPGoNotification - -internal fun PushPushNotification.mapToDto() = - PPGoNotification( - title = notification.title, - body = notification.body, - priority = notification.priority, - campaignId = campaignId, - redirectLink = redirectLink, - ) diff --git a/library/src/main/java/com/pushpushgo/sdk/di/NetworkModule.kt b/library/src/main/java/com/pushpushgo/sdk/di/NetworkModule.kt deleted file mode 100644 index bbc3740e..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/di/NetworkModule.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.pushpushgo.sdk.di - -import android.content.Context -import com.pushpushgo.sdk.network.ApiRepository -import com.pushpushgo.sdk.network.ApiService -import com.pushpushgo.sdk.network.SharedPreferencesHelper -import com.pushpushgo.sdk.network.interceptor.RequestInterceptor -import com.pushpushgo.sdk.network.interceptor.ResponseInterceptor -import com.pushpushgo.sdk.utils.PlatformType -import com.pushpushgo.sdk.utils.getPlatformType -import org.kodein.di.* - -internal class NetworkModule( - context: Context, - apiKey: String, - projectId: String, - isProduction: Boolean, - isDebug: Boolean, - customBaseUrl: String?, -) : DIAware { - companion object { - const val API_KEY = "api_key" - const val PROJECT_ID = "project_id" - } - - private val resolvedBaseUrl = - when { - isProduction -> "https://api.pushpushgo.com" - customBaseUrl != null -> customBaseUrl - else -> "https://api.master1.qappg.co" - } - - override val di by DI.lazy { - constant(tag = API_KEY) with apiKey - constant(tag = PROJECT_ID) with projectId - bind() with singleton { getPlatformType() } - bind() with provider { context } - bind() with singleton { RequestInterceptor() } - bind() with singleton { ResponseInterceptor() } - bind() with singleton { SharedPreferencesHelper(instance()) } - bind() with - singleton { - ApiService( - requestInterceptor = instance(), - responseInterceptor = instance(), - platformType = instance(), - isNetworkDebug = isDebug, - baseUrl = resolvedBaseUrl, - ) - } - bind() with - singleton { - ApiRepository( - apiService = instance(), - context = instance(), - sharedPref = instance(), - projectId = instance(PROJECT_ID), - apiKey = instance(API_KEY), - baseUrl = resolvedBaseUrl, - ) - } - } - - val sharedPref by instance() - val apiRepository by instance() -} diff --git a/library/src/main/java/com/pushpushgo/sdk/di/WorkModule.kt b/library/src/main/java/com/pushpushgo/sdk/di/WorkModule.kt deleted file mode 100644 index f01a7a1f..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/di/WorkModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.pushpushgo.sdk.di - -import android.content.Context -import androidx.work.WorkManager -import com.pushpushgo.sdk.network.SharedPreferencesHelper -import com.pushpushgo.sdk.work.UploadManager -import org.kodein.di.* - -internal class WorkModule( - context: Context, -) : DIAware { - override val di by DI.lazy { - bind() with provider { context } - bind() with singleton { SharedPreferencesHelper(instance()) } - bind() with singleton { WorkManager.getInstance(instance()) } - bind() with singleton { UploadManager(instance(), instance()) } - } - - val uploadManager by instance() -} diff --git a/library/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt b/library/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt deleted file mode 100644 index 373e43db..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/dto/PPGoNotification.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.pushpushgo.sdk.dto - -data class PPGoNotification( - val title: String?, - val body: String?, - val priority: Int = 0, - val campaignId: String, - val redirectLink: String?, -) diff --git a/library/src/main/java/com/pushpushgo/sdk/network/SharedPreferencesHelper.kt b/library/src/main/java/com/pushpushgo/sdk/network/SharedPreferencesHelper.kt deleted file mode 100644 index e73765a2..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/network/SharedPreferencesHelper.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.pushpushgo.sdk.network - -import android.content.Context -import androidx.preference.PreferenceManager.getDefaultSharedPreferences -import com.pushpushgo.sdk.PushPushGo -import com.pushpushgo.sdk.utils.PlatformType -import com.pushpushgo.sdk.utils.getPlatformType -import java.util.UUID - -internal class SharedPreferencesHelper( - context: Context, - prefsName: String? = null, -) { - companion object { - internal const val SUBSCRIBER_ID = "_PushPushGoSDK_sub_id_" - internal const val LAST_FCM_TOKEN = "_PushPushGoSDK_curr_token_" - internal const val LAST_HCM_TOKEN = "_PushPushGoSDK_curr_hms_token_" - internal const val IS_SUBSCRIBED = "_PushPushGoSDK_is_subscribed_" - internal const val ARE_NOTIFICATIONS_BLOCKED = "_PushPushGoSDK_notifications_blocked_" - internal const val CUSTOM_INTENT_FLAGS = "_PushPushGoSDK_custom_intent_flags_" - internal const val LA_SUBSCRIBER_PREFIX = "_PushPushGoSDK_la_sub_" - internal const val INSTALLATION_ID = "_PushPushGoSDK_installation_id_" - private const val MAX_KEYS = 1000 - } - - private val sharedPreferences = - if (prefsName != null) { - context.getSharedPreferences(prefsName, Context.MODE_PRIVATE) - } else { - getDefaultSharedPreferences(context) - } - - var isSubscribed - get() = - sharedPreferences.getBoolean( - IS_SUBSCRIBED, - PushPushGo.isInitialized().takeIf { it }?.let { PushPushGo.getInstance().defaultIsSubscribed } ?: false, - ) - set(value) { - sharedPreferences.edit().putBoolean(IS_SUBSCRIBED, value).apply() - } - - var customIntentFlags - get() = - sharedPreferences.getInt( - CUSTOM_INTENT_FLAGS, - 0, - ) - set(value) { - sharedPreferences.edit().putInt(CUSTOM_INTENT_FLAGS, value).apply() - } - - var areNotificationsBlocked - get() = sharedPreferences.getBoolean(ARE_NOTIFICATIONS_BLOCKED, false) - set(value) { - sharedPreferences.edit().putBoolean(ARE_NOTIFICATIONS_BLOCKED, value).apply() - } - - var subscriberId - get() = sharedPreferences.getString(SUBSCRIBER_ID, "").orEmpty() - set(value) { - sharedPreferences.edit().putString(SUBSCRIBER_ID, value).apply() - } - - /** - * Stable per-installation UUID, generated and persisted on first access. - * Used as `installationId` for Live Activity subscriber registration (backend - * requires a UUID, unlike the Mongo-style subscriberId). - */ - val installationId: String - get() = - sharedPreferences.getString(INSTALLATION_ID, null) - ?: UUID.randomUUID().toString().also { - sharedPreferences.edit().putString(INSTALLATION_ID, it).apply() - } - - var lastFCMToken - get() = sharedPreferences.getString(LAST_FCM_TOKEN, "").orEmpty() - set(value) { - sharedPreferences.edit().putString(LAST_FCM_TOKEN, value).apply() - } - - var lastHCMToken - get() = sharedPreferences.getString(LAST_HCM_TOKEN, "").orEmpty() - set(value) { - sharedPreferences.edit().putString(LAST_HCM_TOKEN, value).apply() - } - - val lastToken - get() = - when (getPlatformType()) { - PlatformType.FCM -> lastFCMToken - PlatformType.HCM -> lastHCMToken - } - - /** LA subscriber id returned by the backend, keyed by live notification id. */ - fun getLiveActivitySubscriberId(liveNotificationId: String): String = - sharedPreferences.getString(LA_SUBSCRIBER_PREFIX + liveNotificationId, "").orEmpty() - - fun setLiveActivitySubscriberId( - liveNotificationId: String, - subscriberId: String, - ) { - sharedPreferences.edit().putString(LA_SUBSCRIBER_PREFIX + liveNotificationId, subscriberId).apply() - } - - fun removeLiveActivitySubscriberId(liveNotificationId: String) { - sharedPreferences.edit().remove(LA_SUBSCRIBER_PREFIX + liveNotificationId).apply() - } - - fun getNotificationId(key: String): Int = sharedPreferences.getInt(key, -1) - - fun setNotificationId( - key: String, - id: Int, - ) { - val allEntries = sharedPreferences.all - if (allEntries.size >= MAX_KEYS) { - val firstKey = allEntries.keys.firstOrNull() - if (firstKey != null) { - sharedPreferences.edit().remove(firstKey).apply() - } - } - sharedPreferences.edit().putInt(key, id).apply() - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/network/data/TokenRequest.kt b/library/src/main/java/com/pushpushgo/sdk/network/data/TokenRequest.kt deleted file mode 100644 index d7d36edc..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/network/data/TokenRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.pushpushgo.sdk.network.data - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -internal data class TokenRequest( - @Json(name = "token") - val token: String, -) diff --git a/library/src/main/java/com/pushpushgo/sdk/network/interceptor/ResponseInterceptor.kt b/library/src/main/java/com/pushpushgo/sdk/network/interceptor/ResponseInterceptor.kt deleted file mode 100644 index 9234daff..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/network/interceptor/ResponseInterceptor.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.pushpushgo.sdk.network.interceptor - -import android.util.JsonReader -import com.pushpushgo.sdk.exception.PushPushException -import com.pushpushgo.sdk.utils.logError -import okhttp3.Interceptor -import okhttp3.Response -import java.io.StringReader - -internal class ResponseInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val response = chain.proceed(chain.request()) - if (!response.isSuccessful) { - val responseBodyCopy = response.peekBody(java.lang.Long.MAX_VALUE).string() - // Only attempt to extract a `message` when the body is actually a JSON - // object; error responses can be HTML (e.g. a gateway 404), which would - // otherwise spam misleading JSON-parsing exceptions. - if (responseBodyCopy.trimStart().startsWith("{")) { - try { - val reader = - JsonReader(StringReader(responseBodyCopy)).apply { - isLenient = true - } - reader.beginObject() - if (reader.nextName() == "message") { - throw PushPushException(reader.nextString()) - } - } catch (e: RuntimeException) { - logError(e) - } - } - } - return response - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/utils/LogUtils.kt b/library/src/main/java/com/pushpushgo/sdk/utils/LogUtils.kt deleted file mode 100644 index 7bc67125..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/utils/LogUtils.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.pushpushgo.sdk.utils - -import android.util.Log -import com.pushpushgo.sdk.PushPushGo - -internal fun logDebug(text: String) { - if (!PushPushGo.isInitialized()) return - if (!PushPushGo.getInstance().isDebug) return - - Log.d(PushPushGo.TAG, text) -} - -internal fun logWarning(text: String) { - if (!PushPushGo.isInitialized()) return - Log.w(PushPushGo.TAG, text) -} - -internal fun logError( - text: String, - exception: Throwable? = null, -) { - if (!PushPushGo.isInitialized()) return - Log.e(PushPushGo.TAG, text, exception) -} - -internal fun logError(exception: Throwable?) { - if (!PushPushGo.isInitialized()) return - Log.e(PushPushGo.TAG, exception?.message, exception) -} diff --git a/library/src/main/java/com/pushpushgo/sdk/utils/StringValidationExtension.kt b/library/src/main/java/com/pushpushgo/sdk/utils/StringValidationExtension.kt deleted file mode 100644 index 9c361200..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/utils/StringValidationExtension.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.pushpushgo.sdk.utils - -import com.pushpushgo.sdk.exception.PushPushException - -internal fun validateApiKey(apiKey: String) { - if (!"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$".toRegex().matches(apiKey)) { - throw PushPushException("Invalid API key! Current API key: `$apiKey`") - } -} - -internal fun validateProjectId(projectId: String) { - if (!"[a-z0-9]{24}".toRegex().matches(projectId)) { - throw PushPushException("Invalid project ID! Current project ID: `$projectId`") - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/work/UploadDelegate.kt b/library/src/main/java/com/pushpushgo/sdk/work/UploadDelegate.kt deleted file mode 100644 index 8d7c3398..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/work/UploadDelegate.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.pushpushgo.sdk.work - -import com.pushpushgo.sdk.PushPushGo -import com.pushpushgo.sdk.data.EventType -import com.pushpushgo.sdk.utils.logDebug -import com.pushpushgo.sdk.utils.logError -import kotlinx.coroutines.* -import org.json.JSONObject - -internal class UploadDelegate { - private val uploadScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - private val errorHandler = CoroutineExceptionHandler { _, e -> logError(e) } - - suspend fun doNetworkWork( - type: String?, - data: String?, - ) { - if (PushPushGo.getInstance().getSubscriberId().isBlank() && type != UploadWorker.REGISTER) { - return logDebug("UploadWorker: skipped. Reason: not subscribed") - } - - with(PushPushGo.getInstance().getNetwork()) { - when (type) { - UploadWorker.REGISTER -> registerToken(data) - UploadWorker.UNREGISTER -> unregisterSubscriber() - else -> logDebug("Unknown upload data type") - } - } - } - - fun sendEvent( - type: EventType, - buttonId: Int, - campaign: String, - projectId: String?, - subscriberId: String?, - ) { - uploadScope.launch(errorHandler) { - PushPushGo.getInstance().getNetwork().sendEvent( - type = type, - buttonId = buttonId, - campaign = campaign, - project = projectId, - subscriber = subscriberId, - ) - } - } - - fun sendBeacon(beacon: JSONObject) { - if (PushPushGo.getInstance().getSubscriberId().isBlank()) { - logDebug("Beacon not sent. Reason: not subscribed") - return - } - - uploadScope.launch(errorHandler) { - PushPushGo.getInstance().getNetwork().sendBeacon(beacon.toString()) - } - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/work/UploadManager.kt b/library/src/main/java/com/pushpushgo/sdk/work/UploadManager.kt deleted file mode 100644 index 36564b86..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/work/UploadManager.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.pushpushgo.sdk.work - -import androidx.work.* -import com.pushpushgo.sdk.network.SharedPreferencesHelper -import com.pushpushgo.sdk.utils.logDebug -import com.pushpushgo.sdk.work.UploadWorker.Companion.DATA -import com.pushpushgo.sdk.work.UploadWorker.Companion.REGISTER -import com.pushpushgo.sdk.work.UploadWorker.Companion.TYPE -import com.pushpushgo.sdk.work.UploadWorker.Companion.UNREGISTER -import java.util.concurrent.TimeUnit - -internal class UploadManager( - private val workManager: WorkManager, - private val sharedPref: SharedPreferencesHelper, -) { - companion object { - const val UPLOAD_DELAY = 10L - const val UPLOAD_RETRY_DELAY = 30L - } - - fun sendRegister(token: String?) { - logDebug("Register enqueued") - - enqueueJob(REGISTER, isMustRunImmediately = true, data = token) - listOf(UNREGISTER).forEach { - workManager.cancelAllWorkByTag(it) - } - } - - fun sendUnregister() { - if (!sharedPref.isSubscribed) { - logDebug("Can't unregister, because device not registered. Skipping") - return - } - - logDebug("Unregister enqueued") - - enqueueJob(UNREGISTER, isMustRunImmediately = true) - listOf(REGISTER).forEach { - workManager.cancelAllWorkByTag(it) - } - } - - private fun enqueueJob( - name: String, - data: String? = null, - isMustRunImmediately: Boolean = false, - ) { - workManager.enqueueUniqueWork( - name, - if (name == REGISTER || name == UNREGISTER) ExistingWorkPolicy.KEEP else ExistingWorkPolicy.APPEND, - OneTimeWorkRequestBuilder() - .setInputData( - workDataOf(TYPE to name, DATA to data), - ).setBackoffCriteria(BackoffPolicy.LINEAR, UPLOAD_RETRY_DELAY, TimeUnit.SECONDS) - .setInitialDelay(if (isMustRunImmediately || isJobAlreadyEnqueued(name)) 0 else UPLOAD_DELAY, TimeUnit.SECONDS) - .setConstraints( - Constraints - .Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build(), - ).build(), - ) - } - - private fun isJobAlreadyEnqueued(name: String) = - try { - workManager.getWorkInfosForUniqueWork(name).get().any { - it.state == WorkInfo.State.ENQUEUED - } - } catch (e: InterruptedException) { - false - } -} diff --git a/library/src/main/java/com/pushpushgo/sdk/work/UploadWorker.kt b/library/src/main/java/com/pushpushgo/sdk/work/UploadWorker.kt deleted file mode 100644 index 69398d61..00000000 --- a/library/src/main/java/com/pushpushgo/sdk/work/UploadWorker.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.pushpushgo.sdk.work - -import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.pushpushgo.sdk.utils.logDebug -import com.pushpushgo.sdk.utils.logError -import kotlinx.coroutines.coroutineScope - -internal class UploadWorker( - context: Context, - parameters: WorkerParameters, -) : CoroutineWorker(context, parameters) { - companion object { - const val TYPE = "type" - const val DATA = "data" - - const val REGISTER = "register" - const val UNREGISTER = "unregister" - } - - private val delegate by lazy { UploadDelegate() } - - override suspend fun doWork(): Result = - coroutineScope { - logDebug("UploadWorker: started") - - val type = inputData.getString(TYPE) - val data = inputData.getString(DATA) - - try { - delegate.doNetworkWork(type, data) - } catch (e: Throwable) { - logError("UploadWorker error", e) - - return@coroutineScope when { - "Please configure FCM keys and senderIds on your " in e.message.orEmpty() -> Result.failure() - type == REGISTER || type == UNREGISTER -> Result.retry() - else -> Result.failure() - } - } - - logDebug("UploadWorker: success") - - Result.success() - } -} diff --git a/library/src/test/java/com/pushpushgo/sdk/BeaconBuilderTest.kt b/library/src/test/java/com/pushpushgo/sdk/BeaconBuilderTest.kt deleted file mode 100644 index 7d8c7e78..00000000 --- a/library/src/test/java/com/pushpushgo/sdk/BeaconBuilderTest.kt +++ /dev/null @@ -1,361 +0,0 @@ -package com.pushpushgo.sdk - -import com.pushpushgo.sdk.exception.PushPushException -import com.pushpushgo.sdk.work.UploadDelegate -import io.mockk.* -import io.mockk.impl.annotations.MockK -import org.json.JSONArray -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -internal class BeaconBuilderTest { - @MockK - lateinit var uploadDelegate: UploadDelegate - - private lateinit var beaconBuilder: BeaconBuilder - - @Before - fun setUp() { - MockKAnnotations.init(this) - } - - @Test - fun `set string selector`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.set("Selector", "Value") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["Selector"] == "Value" - }, - ) - } - } - - @Test - fun `set boolean selector`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.set("Selector", true) - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["Selector"] == true - }, - ) - } - } - - @Test - fun `set char selector`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.set("Selector", 'A') - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["Selector"] == 'A' - }, - ) - } - } - - @Test - fun `set number selector`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.set("Selector", 421) - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["Selector"] == 421 - }, - ) - } - } - - @Test - fun `set unsupported selector`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.set("Selector", JSONObject()) - - val exception = requireNotNull(runCatching { beaconBuilder.send() }.exceptionOrNull()) - assertEquals(PushPushException::class.java, exception::class.java) - assertEquals("Invalid type of beacon selector value. Supported types: boolean, string, char, number", exception.message) - - verify { uploadDelegate wasNot Called } - } - - @Test - fun `append tag with label`() { - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.appendTag("tag1", "label1") - - val tags = beaconBuilder.getTags() - - assertEquals(1, tags.size) - assertEquals(tags[0].first, "tag1") - assertEquals(tags[0].second, "label1") - } - - @Test - fun `append tag without label`() { - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.appendTag("tag1") - - val tags = beaconBuilder.getTags() - - assertEquals(1, tags.size) - assertEquals(tags[0].first, "tag1") - assertEquals(tags[0].second, "default") - } - - @Test - fun `append many tags with label`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder - .appendTag("tag1", "label1") - .appendTag("tag2", "label2") - .appendTag("tag3", "label3") - - beaconBuilder.send() - val tags = beaconBuilder.getTags() - - assertEquals(3, tags.size) - assertEquals(tags[0].first, "tag1") - assertEquals(tags[0].second, "label1") - assertEquals(tags[2].first, "tag3") - assertEquals(tags[2].second, "label3") - - verify { - uploadDelegate.sendBeacon( - withArg { - val tag = (it["tags"] as JSONArray).getJSONObject(0) - - assertEquals("tag1", tag["tag"]) - assertEquals("label1", tag["label"]) - assertEquals("append", tag["strategy"]) - assertEquals(0, tag["ttl"]) - }, - ) - } - } - - @Test - fun `remove tag`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.removeTag("tag1", "tag2") - beaconBuilder.send() - - val tags = beaconBuilder.getTagsToDelete() - - assertEquals(2, tags.size) - assertEquals(tags[0], "tag1") - assertEquals(tags[1], "tag2") - - verify { - uploadDelegate.sendBeacon( - match { - it["tagsToDelete"].toString() == """["tag1","tag2"]""" - }, - ) - } - } - - @Test - fun `remove tag with a custom label`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.removeTags(mapOf("tag_name" to "test_label")) - beaconBuilder.send() - - val tags = beaconBuilder.getTagsToDelete() - - assertEquals(1, tags.size) - assertEquals(tags[0], "tag_name") - - verify { - uploadDelegate.sendBeacon( - match { - it["tagsToDelete"].toString() == """[{"tag":"tag_name","label":"test_label"}]""" - }, - ) - } - } - - @Test - fun `remove same tag with different labels`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.removeTags( - listOf( - "value1" to "test_label", - "value3" to "test_label", - "value1" to "other_label", - ), - ) - beaconBuilder.send() - - val tags = beaconBuilder.getTagsToDelete() - - assertEquals(3, tags.size) - - verify { - uploadDelegate.sendBeacon( - match { - it["tagsToDelete"].toString() == - """[{"tag":"value1","label":"test_label"},{"tag":"value3","label":"test_label"},{"tag":"value1","label":"other_label"}]""" - }, - ) - } - } - - @Test - fun `set custom id`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.setCustomId("id1") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["customId"] == "id1" - }, - ) - } - } - - @Test - fun `send empty beacon`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it.toString() == "{}" - }, - ) - } - } - - @Test - fun `assign to group`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.assignToGroup("my-segment-123") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["assignToGroup"] == "my-segment-123" - }, - ) - } - } - - @Test - fun `unassign from group`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder.unassignFromGroup("my-segment-333") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["unassignFromGroup"] == "my-segment-333" - }, - ) - } - } - - @Test - fun `assign and unassign from groups simultaneously`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder - .assignToGroup("my-segment-123") - .unassignFromGroup("my-segment-333") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["assignToGroup"] == "my-segment-123" && - it["unassignFromGroup"] == "my-segment-333" - }, - ) - } - } - - @Test - fun `assign to group with other beacon data`() { - every { uploadDelegate.sendBeacon(any()) } just Runs - - beaconBuilder = BeaconBuilder(uploadDelegate) - - beaconBuilder - .setCustomId("xxsampleId") - .assignToGroup("my-segment-123") - .appendTag("tag1", "label1") - - beaconBuilder.send() - - verify { - uploadDelegate.sendBeacon( - match { - it["customId"] == "xxsampleId" && - it["assignToGroup"] == "my-segment-123" && - it.has("tags") - }, - ) - } - } -} diff --git a/library/src/test/java/com/pushpushgo/sdk/utils/StringValidationExtensionKtTest.kt b/library/src/test/java/com/pushpushgo/sdk/utils/StringValidationExtensionKtTest.kt deleted file mode 100644 index 3a69e8f2..00000000 --- a/library/src/test/java/com/pushpushgo/sdk/utils/StringValidationExtensionKtTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.pushpushgo.sdk.utils - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -class StringValidationExtensionKtTest { - @Test - fun `check valid api key`() { - val result = runCatching { validateApiKey("57118b49-eb83-4de4-aea9-872144b443fc") } - assertTrue(result.isSuccess) - } - - @Test - fun `check invalid api key`() { - val result = runCatching { validateApiKey("empty1") } - assertTrue(result.isFailure) - assertEquals("Invalid API key! Current API key: `empty1`", result.exceptionOrNull()?.message) - } - - @Test - fun `check valid project id`() { - val result = kotlin.runCatching { validateProjectId("5d411352784425000bd02a15") } - assertTrue(result.isSuccess) - } - - @Test - fun `check invalid project id`() { - val result = runCatching { validateProjectId("empty2") } - assertTrue(result.isFailure) - assertEquals("Invalid project ID! Current project ID: `empty2`", result.exceptionOrNull()?.message) - } -} diff --git a/library/.gitignore b/push/.gitignore similarity index 100% rename from library/.gitignore rename to push/.gitignore diff --git a/push/CHANGELOG.md b/push/CHANGELOG.md new file mode 100644 index 00000000..8bb54a5c --- /dev/null +++ b/push/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +## [4.0.0] – Breaking release + +### Breaking changes + +#### Distribution +- The SDK has migrated from JitPack to Maven Central. +- The JitPack Maven repository (`https://jitpack.io`) can be removed from your Gradle configuration. + +#### Min SDK Version +- Minimum Android SDK version increased from 23 to 26. + +#### SDK entry point & initialization +- **Replaced `PushPushGo` with `PushNotifications`** as the main public API. +- `PushNotifications` is now a process-wide object; replace + `PushNotifications.getInstance().method()` with `PushNotifications.method()`. +- Initialization is now explicit: + - `PushNotifications.initialize(Application)` + - `PushNotifications.initialize(Application, Config)` +- WorkManager must be initialized before the SDK. AndroidX Startup handles this automatically unless + the application disables WorkManager's automatic initializer. + +#### Subscription API redesign +- Removed legacy subscription methods: + - `createSubscriber()` + - `registerSubscriber()` + - `unregisterSubscriber(...)` +- Introduced a unified subscription API: + - `subscribe()` / `unsubscribe()` (Kotlin coroutines) + - `subscribeAsync()` / `unsubscribeAsync()` (Java-friendly `CompletableFuture`) +- Removed `subscribeNow()`, `unsubscribeNow()`, `subscribeNowFuture()`, and + `unsubscribeNowFuture()`; use the unified methods above. + +#### Async API changes +- **Removed Guava `ListenableFuture` from the public API**. +- All async operations now use: + - `suspend` functions for Kotlin consumers + - `CompletableFuture` for Java consumers + +#### Notification model +- Replaced `PPGoNotification` with **`PushPushGoNotification`**. +- All **`PushPushGoNotification`** fields are now non-nullable. + +#### Handlers +- Renamed handlers: + - `setNotificationHandler(...)` → `setNotificationClickHandler(...)` + - `setOnInvalidProjectIdHandler(...)` → `setInvalidProjectIdHandler(...)` +- Handler and error callback APIs now use named SAM interfaces: + - `NotificationClickHandler` + - `InvalidProjectIdHandler` + - `PushNotificationsErrorCallback` +- Callbacks can be configured before initialization and survive deinitialization. +- Passing `null` to a callback setter restores its default behavior. + +#### Beacon +- Selector assignment is now explicit and type-specific: + - `set(key, String)` + - `set(key, Number)` + - `set(key, Boolean)` + - `set(key, Char)` +- Introduced `BeaconTagStrategy` enum (`APPEND`, `REWRITE`); string-based strategies are no longer supported. +- Removed `setCustomId(Int?)` method; custom IDs must now be provided using `setCustomId(String?)`. +- Replace `PushNotifications.getInstance().createBeacon()...send()` with + `BeaconBuilder()...build()` followed by `PushNotifications.sendBeacon(...)` or + `PushNotifications.sendBeaconAsync(...)`. + +#### Project migration API +- Removed `migrateToNewProject(...)`. +- To switch an initialized SDK to another explicit configuration, first call the + suspending `PushNotifications.deinitialize()` method and, after it completes, + call `PushNotifications.initialize(application, newConfig)`, then explicitly + call `PushNotifications.subscribe()` to subscribe to the new project. +- Java callers should wait for `PushNotifications.deinitializeAsync()` before + calling `initialize(...)`. +- Deinitialization removes Live Activities first, then unsubscribes the current + subscriber and clears its persisted project data. If any step fails, the SDK + remains initialized and the operation fails. Live Activities already removed + stay removed. + +#### Live Activities +- Live Activity APIs moved from `PushNotifications` to the + `PushNotifications.liveActivities` facade. + +#### Public configuration API +- The following configuration properties are no longer publicly mutable and must be set via explicit setter methods: + - `notificationClickHandler` + - `invalidProjectIdHandler` + - `customClickIntentFlags` +- `defaultIsSubscribed` and `setDefaultIsSubscribed(...)` were removed from the public API. + +#### Core dependency +- The SDK now depends on a shared internal **core** module. +- The SDK provides an implementation of the core module’s `PushSubscriptionProvider` interface, which can be reused by other PushPushGo SDKs to interact with push functionality. + +--- + +### Migration guide + +- This release **requires code changes** and is not source-compatible with `3.x`. +- Migrate: + - `PushPushGo` → `PushNotifications` + - `PushNotifications.getInstance().method()` → `PushNotifications.method()` + - `ListenableFuture` → `CompletableFuture` + - Legacy subscription calls → new unified subscription API + - `migrateToNewProject(...)` → `deinitialize()`, then `initialize(...)`, followed by an + explicit `subscribe()` + - Direct Live Activity methods → `PushNotifications.liveActivities` + - `createBeacon()...send()` → `BeaconBuilder()...build()` followed by `sendBeacon(...)` + - String-based tag strategies (e.g. `"append"`, `"rewrite"`) → `BeaconTagStrategy.APPEND` / `BeaconTagStrategy.REWRITE` + - Integer-based custom IDs → string-based custom IDs diff --git a/DEEPLINKS.md b/push/DEEPLINKS.md similarity index 82% rename from DEEPLINKS.md rename to push/DEEPLINKS.md index 0a25dda3..e64fedd3 100644 --- a/DEEPLINKS.md +++ b/push/DEEPLINKS.md @@ -5,8 +5,6 @@ This document explains how push notifications sent via PushPushGo handle URLs application must do to route the user to a specific screen after a notification click. -> Applies to the `com.pushpushgo:sdk` (module `library/`). - --- ## Table of contents @@ -36,7 +34,7 @@ displayed, the SDK builds a `PendingIntent` that: plus the URL as extras. Key extra keys (declared in -`com.pushpushgo.sdk.push.PushNotificationDelegate`): +`com.pushpushgo.sdk.push.push.PushNotificationDelegate`): | Key | Constant | Description | |--------------------|--------------------------|--------------------------------------------| @@ -47,11 +45,6 @@ Key extra keys (declared in | `project` | `PROJECT_ID_EXTRA` | Project id, used to validate the click | | `subscriber` | `SUBSCRIBER_ID_EXTRA` | Subscriber id | -The full raw payload (`notification`, `actions`, `redirectLink`, `image`, -`icon`, ...) is also passed as an extra named `notification` so that the SDK -can reconstruct the `PushPushNotification` object via -`deserializeNotificationData(intent.extras)`. - --- ## Payload reference @@ -94,7 +87,7 @@ your existing navigation stack is lost. ### 2. Forward click intents to the SDK -Call `PushPushGo.getInstance().handleBackgroundNotificationClick(intent)` in +Call `PushNotifications.handleBackgroundNotificationClick(intent)` in both `onCreate()` and `onNewIntent()` of your launcher activity: ```kotlin @@ -105,14 +98,14 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) // Handle the case when the app was launched from a notification - PushPushGo.getInstance().handleBackgroundNotificationClick(intent) + PushNotifications.handleBackgroundNotificationClick(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle clicks that arrive while the activity is already running setIntent(intent) - PushPushGo.getInstance().handleBackgroundNotificationClick(intent) + PushNotifications.handleBackgroundNotificationClick(intent) } } ``` @@ -120,9 +113,10 @@ class MainActivity : AppCompatActivity() { `handleBackgroundNotificationClick` will: 1. Validate the `project` extra against the configured project id. If they do - not match, `onInvalidProjectIdHandler` is invoked and nothing else happens. + not match, the `InvalidProjectIdHandler` is invoked and nothing else happens. 2. Cancel the local system notification. -3. Dispatch the URL to `notificationHandler(context, url, overrideFlags)`. +3. Dispatch the URL to the configured + `NotificationClickHandler.onNotificationClick(context, url, overrideFlags)`. 4. Send a `CLICKED` analytics event (body click or action-button click depending on the `button` extra). 5. Clear the `project` extra so the same click is not processed twice. @@ -131,29 +125,28 @@ class MainActivity : AppCompatActivity() { ## Default behavior (opening the URL) -The SDK's default `notificationHandler` is: +The SDK's default `NotificationClickHandler` is: ```kotlin -// com.pushpushgo.sdk.PushPushGo -var notificationHandler: NotificationHandler = { context, url, overrideFlags -> - handleNotificationLinkClick(context, url, overrideFlags) +// com.pushpushgo.sdk.push.DefaultNotificationClickHandler +internal class DefaultNotificationClickHandler : NotificationClickHandler { + override fun onNotificationClick(context: Context, url: String, overrideFlags: Int) { + Intent.parseUri(url, 0).let { + it.addFlags(overrideFlags) + try { + context.startActivity(it) + } catch (e: ActivityNotFoundException) { + // Fallback: show the URL as a toast + } + } + } } ``` -which boils down to: +After overriding the handler, restore the default by passing `null`: ```kotlin -// com.pushpushgo.sdk.push.NotificationUtils -internal fun handleNotificationLinkClick(context, uri, overrideFlags) { - Intent.parseUri(uri, 0).let { - it.addFlags(overrideFlags) - try { - context.startActivity(it) - } catch (e: ActivityNotFoundException) { - // Fallback: show the URL as a toast - } - } -} +PushNotifications.setNotificationClickHandler(null) ``` The default `overrideFlags` passed by `handleBackgroundNotificationClick` is @@ -243,16 +236,18 @@ You can fully replace the default behavior (e.g. to route through your own navigation component, or to suppress URL opening): ```kotlin -PushPushGo.getInstance().notificationHandler = { context, url, overrideFlags -> +PushNotifications.setNotificationClickHandler { context, url, overrideFlags -> // Example: route through your own deep-link dispatcher MyDeepLinkRouter.handle(context, url) } ``` -The `NotificationHandler` type is: +The `NotificationClickHandler` type is: ```kotlin -typealias NotificationHandler = (context: Context, url: String, overrideFlags: Int) -> Unit +fun interface NotificationClickHandler { + fun onNotificationClick(context: Context, url: String, overrideFlags: Int) +} ``` This handler is called both by `handleBackgroundNotificationClick` and by the @@ -266,7 +261,7 @@ If you need a different set of `Intent.FLAG_*` than the default `FLAG_ACTIVITY_NEW_TASK`, persist them through the SDK: ```kotlin -PushPushGo.getInstance().setCustomClickIntentFlags( +PushNotifications.setCustomClickIntentFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP, @@ -274,7 +269,7 @@ PushPushGo.getInstance().setCustomClickIntentFlags( ``` These flags are applied when the SDK builds the launcher `PendingIntent` -and can be read back via `getCustomClickIntentFlags()`. +and can be read back via `PushNotifications.customClickIntentFlags`. --- @@ -297,7 +292,8 @@ produces the same flow as a body click, with the following differences: - The `button` extra is set to the 1-based index (`1` for the first button, `2` for the second, ...). -- The URL dispatched to `notificationHandler` comes from `actions[i].link` +- The URL dispatched to the configured `NotificationClickHandler` comes from + `actions[i].link` (via the `link` extra), not from `redirectLink`. The `button` id is reported in the `CLICKED` analytics event. @@ -310,12 +306,16 @@ If you only need to inspect the payload (e.g. to implement your own routing), use `getNotificationDetails`: ```kotlin -val ppgoNotification: PPGoNotification? = - PushPushGo.getInstance().getNotificationDetails(notificationDataMap) +val pushPushGoNotification: PushPushGoNotification? = + PushNotifications.getNotificationDetails(notificationDataMap) ``` -or, inside an activity, reconstruct the full payload directly from the -intent extras — the SDK stores the raw JSON under the `notification` key. +or pass the notification intent directly: + +```kotlin +val pushPushGoNotification: PushPushGoNotification? = + PushNotifications.getNotificationDetails(intent) +``` --- @@ -331,7 +331,7 @@ intent extras — the SDK stores the raw JSON under the `notification` key. `adb shell pm get-app-links `. - **Click is ignored entirely.** `project` extra did not match the project id the SDK was initialized with. Check - `PushPushGo.getInstance().getProjectId()` and the `project` field in the + `PushNotifications.getProjectId()` and the `project` field in the payload. - **Toast with the URL appears instead of the app opening.** The default handler caught an `ActivityNotFoundException`. Either no activity handles diff --git a/LIVE_ACTIVITIES.md b/push/LIVE_ACTIVITIES.md similarity index 64% rename from LIVE_ACTIVITIES.md rename to push/LIVE_ACTIVITIES.md index a3d288b9..a4f2be28 100644 --- a/LIVE_ACTIVITIES.md +++ b/push/LIVE_ACTIVITIES.md @@ -14,9 +14,9 @@ progress bar with break indicators. | Requirement | Notes | |---|---| -| Android 16 (API 36) device | On older devices Live Activity pushes are ignored — check `isLiveActivitiesSupported()` | -| PushPushGo SDK integrated | Push notifications must already work (see the main [README](README.md)) | -| Registered subscriber | Call `createSubscriber()` / `registerSubscriber()` before subscribing to a Live Activity | +| Android 16 (API 36) device | On older devices Live Activity pushes are ignored — check `liveActivities.isSupported()` | +| PushNotifications SDK integrated | Push notifications must already work (see the [README](README.md)) | +| Registered subscriber | Call `subscribe()` before subscribing to a Live Activity | | `POST_NOTIFICATIONS` granted | Standard runtime notification permission | | FCM | Live Activity pushes are delivered as FCM data messages | @@ -35,11 +35,11 @@ notification campaign (PUT /live-data), │ ▲ ProgressStyle notification, │ reports analytics events device subscribes to the live notification ◄──────────────┘ - (POST /subscribers — done by the SDK) + (`liveActivities.subscribe` — done by the SDK) ``` 1. A live notification campaign is created and submitted on the PushPushGo side. -2. The device **subscribes** to that campaign with `subscribeToLiveActivity(id)`. +2. The device **subscribes** to that campaign with `liveActivities.subscribe(id)`. 3. The backend sends `start` / `update` / `end` data pushes; the SDK renders and updates the notification. Score and phase changes are **server-side** — the app never drives the match state. @@ -52,27 +52,22 @@ notification campaign (PUT /live-data), │ ### 1. Subscribe / unsubscribe ```kotlin -val ppg = PushPushGo.getInstance() +val liveActivities = PushNotifications.liveActivities // The device must already be a registered push subscriber. -if (ppg.isLiveActivitiesSupported()) { - Futures.addCallback( - ppg.subscribeToLiveActivity(""), - object : FutureCallback { - override fun onSuccess(laSubscriberId: String) { /* subscribed */ } - override fun onFailure(t: Throwable) { /* handle error */ } - }, - ContextCompat.getMainExecutor(context), - ) +if (liveActivities.isSupported()) { + val laSubscriberId = liveActivities.subscribe("") + // subscribed; laSubscriberId is also persisted by the SDK } // Later: -ppg.unsubscribeFromLiveActivity("") +liveActivities.unsubscribe("") ``` -- `subscribeToLiveActivity` registers the device on the backend and returns the - Live Activity subscriber id. The SDK persists it, so `unsubscribeFromLiveActivity` - needs only the live notification id. +- Both methods are suspending (`subscribe` returns the backend LA subscriber id; + the SDK persists it, so `unsubscribe` only needs the live notification id). + Java callers can use `subscribeAsync` and `unsubscribeAsync`, which return a + `CompletableFuture`. - Subscribing to an already running activity renders its current state at once. ### 2. Handle clicks @@ -84,28 +79,28 @@ Add this to your launcher (main) activity, next to the existing override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... - PushPushGo.getInstance().handleBackgroundNotificationClick(intent) - PushPushGo.getInstance().handleLiveActivityClick(intent) + PushNotifications.handleBackgroundNotificationClick(intent) + PushNotifications.liveActivities.handleClick(intent) } -override fun onNewIntent(intent: Intent?) { +override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - PushPushGo.getInstance().handleBackgroundNotificationClick(intent) - PushPushGo.getInstance().handleLiveActivityClick(intent) + PushNotifications.handleBackgroundNotificationClick(intent) + PushNotifications.liveActivities.handleClick(intent) } ``` -`handleLiveActivityClick`: +`liveActivities.handleClick`: - reports the click analytics event (notification body and action buttons are distinguished automatically), - opens the deep link carried by the notification through the SDK's - `notificationHandler` — the same routing used for regular push clicks, + notification click handler — the same routing used for regular push clicks, - returns the deep link (or `null` if the intent was not a Live Activity click). To handle the link yourself, pass `openDeepLink = false`: ```kotlin -val deepLink = PushPushGo.getInstance().handleLiveActivityClick(intent, openDeepLink = false) +val deepLink = PushNotifications.liveActivities.handleClick(intent, openDeepLink = false) if (deepLink != null) { // custom navigation } @@ -118,11 +113,11 @@ app deep links (`app://...`). By default the SDK resolves them with a standard `ACTION_VIEW` intent: web links open the browser, app links open the activity with a matching `intent-filter`. -For custom routing override `notificationHandler` once — it then applies to both +For custom routing override the click handler once — it then applies to both regular pushes and Live Activities: ```kotlin -PushPushGo.getInstance().notificationHandler = { context, url, overrideFlags -> +PushNotifications.setNotificationClickHandler { context, url, overrideFlags -> // e.g. route app:///beacons to a specific screen } ``` @@ -131,14 +126,15 @@ PushPushGo.getInstance().notificationHandler = { context, url, overrideFlags -> | Method | Description | |---|---| -| `isLiveActivitiesSupported(): Boolean` | `true` on API 36+ | -| `subscribeToLiveActivity(id): ListenableFuture` | Subscribes the device to a live notification, returns the LA subscriber id | -| `unsubscribeFromLiveActivity(id): ListenableFuture` | Unsubscribes the device | -| `getLiveActivitySubscriberId(id): String` | Persisted LA subscriber id (empty if not subscribed) | -| `getActiveLiveActivities(): List` | Currently tracked (rendered) activities | -| `isLiveActivityActive(id): Boolean` | Whether a given activity is currently active | -| `handleLiveActivityClick(intent, openDeepLink = true): String?` | Click analytics + deep link handling (see above) | -| `simulateLiveActivityPush(data: Map)` | Feeds a push envelope into the rendering pipeline — testing only | +| `liveActivities.isSupported(): Boolean` | `true` on API 36+ | +| `liveActivities.subscribe(id): String` | Suspends while subscribing the device and returns the LA subscriber id | +| `liveActivities.subscribeAsync(id): CompletableFuture` | Java-friendly asynchronous subscription wrapper | +| `liveActivities.unsubscribe(id): Unit` | Suspends while unsubscribing the device | +| `liveActivities.unsubscribeAsync(id): CompletableFuture` | Java-friendly asynchronous unsubscription wrapper | +| `liveActivities.getSubscriberId(id): String` | Persisted LA subscriber id (empty if not subscribed) | +| `liveActivities.getActiveActivities(): List` | Currently tracked (rendered) activities | +| `liveActivities.isActive(id): Boolean` | Whether a given activity is currently active | +| `liveActivities.handleClick(intent, openDeepLink = true): String?` | Click analytics + deep link handling (see above) | All Live Activity APIs are safe to call on any API level; on devices below API 36 they degrade gracefully (no rendering). @@ -191,26 +187,3 @@ The SDK reports Live Activity statistics automatically — no integration needed | `clicked` | Tap on the notification body | | `clicked_1` / `clicked_2` | Tap on the first / second action button | | `closed` | The user dismissed the notification (swipe or `CLOSE` button) | - -## Testing without a backend - -The sample app (`sample/`, *Live Activities* screen) demonstrates the full -integration, including a local simulation mode that drives the -parse → manage → render pipeline through `simulateLiveActivityPush`: - -```kotlin -PushPushGo.getInstance().simulateLiveActivityPush( - mapOf( - "type" to "live_notification", - "liveNotificationId" to "demo-match-1", - "event" to "start", // start | update | end - "template" to "FOOTBALL_MATCH_TRACKING", - "configuration" to configurationJson, // static config; required on start - "liveData" to liveDataJson, // score / status / statusChangedAt - // "hotMessage" to hotMessageJson, // optional transient message - ), -) -``` - -See `sample/src/main/java/com/pushpushgo/sample/activity/LiveActivityDemoActivity.kt` -for complete envelope examples. diff --git a/push/README.md b/push/README.md new file mode 100644 index 00000000..cae3efa7 --- /dev/null +++ b/push/README.md @@ -0,0 +1,384 @@ +# PushPushGo PushNotifications SDK + +Android SDK for integrating push notifications into your application. Supports both **FCM (Firebase Cloud Messaging)** and **HMS (Huawei Push Kit)**. + +## Table of Contents + +- [Preparation](#preparation) +- [Installation](#installation) + - [FCM (Firebase Cloud Messaging)](#fcm-firebase-cloud-messaging) + - [HMS (Huawei Push Kit)](#hms-huawei-push-kit) +- [Configuration](#configuration) + - [AndroidManifest.xml](#androidmanifestxml) + - [Application initialization](#application-initialization) + - [Notification UI customization](#notification-ui-customization) +- [Handling notification clicks](#handling-notification-clicks) +- [Basic usage](#basic-usage) + - [Push subscription](#push-subscription) + - [Beacons, tags, and dynamic groups](#beacons-tags-and-dynamic-groups) +- [Live Activities](#live-activities) + +## Preparation + +1. Remove other push SDKs or custom FCM/HMS implementations. +2. Connect your app to a push provider. +3. Prepare provider configuration files: + - FCM: `google-services.json` + - HMS: `agconnect-services.json` +4. Integrate the provider in the PushPushGo app: + - Project → Settings → Integration + - See [FCM](#fcm-firebase-cloud-messaging) or [HMS](#hms-huawei-push-kit) for details +5. Collect your PushPushGo Project ID and API Key. + +## Installation + +### Requirements + +- Android API 26+ + +Choose installation path depending on your provider. + +## FCM (Firebase Cloud Messaging) + +### Provider credentials + +1. Open **Firebase Console**. +2. Navigate to **Project settings** → **Cloud Messaging**. +3. Click **Manage service accounts**. +4. Select your service account email. +5. Open the **Keys** tab. +6. Click **Add key** → **Create new key**. +7. Choose **JSON** format and download the file. +8. Upload the JSON file in the PushPushGo **FCM** integration section. + +### FCM configuration + +Place `google-services.json` in the app module root: + +``` +app/google-services.json +``` + +### Gradle setup + +```toml +# libs.versions.toml + +[versions] +firebase-bom = "34.1.0" +firebase-messaging = "25.0.0" +google-gms-google-services = "4.4.3" +pushpushgo-sdk-push = "4.0.0" + +[libraries] +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebase-messaging" } +pushpushgo-sdk-push = { module = "com.pushpushgo:sdk-push", version.ref = "pushpushgo-sdk-push" } + +[plugins] +google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "google-gms-google-services" } +``` + +```kotlin +// app/build.gradle.kts +plugins { + alias(libs.plugins.google.gms.google.services) +} + +dependencies { + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.messaging) + implementation(libs.pushpushgo.sdk.push) +} +``` + +```kotlin +// build.gradle.kts +plugins { + alias(libs.plugins.google.gms.google.services) apply false +} +``` + +## HMS (Huawei Push Kit) + +### Provider credentials + +1. Open **Huawei Developers Console**. +2. Navigate to your project. +3. Open **Project settings**. +4. Collect the required values: + - `appId` + - `authUrl` + - `pushUrl` + - `appSecret` +5. Provide these credentials in the PushPushGo **HMS** integration section. + +### HMS configuration + +Place `agconnect-services.json` in the app module root: + +``` +app/agconnect-services.json +``` + +### Gradle setup + +```toml +# libs.versions.toml + +[versions] +hms-agconnect = "1.9.1.304" +hms-push = "6.13.0.300" +hms-update = "5.0.2.300" +pushpushgo-sdk-push = "4.0.0" + +[libraries] +hms-agconnect = { module = "com.huawei.agconnect:agconnect-core", version.ref = "hms-agconnect" } +hms-push = { module = "com.huawei.hms:push", version.ref = "hms-push" } +hms-update = { module = "com.huawei.hms:update", version.ref = "hms-update" } + +pushpushgo-sdk-push = { module = "com.pushpushgo:sdk-push", version.ref = "pushpushgo-sdk-push" } + +``` + +```kotlin +// app/build.gradle.kts +plugins { + id("com.huawei.agconnect") +} + +dependencies { + implementation(libs.hms.agconnect) + implementation(libs.hms.push) + implementation(libs.hms.update) + implementation(libs.pushpushgo.sdk.push) +} +``` + +```kotlin +// build.gradle.kts +plugins { + id("com.huawei.agconnect") apply false +} +``` + +```kotlin +// settings.gradle.kts +pluginManagement { + repositories { + maven(url = "https://developer.huawei.com/repo") + } + + resolutionStrategy { + eachPlugin { + if (requested.id.id == "com.huawei.agconnect") { + useModule("com.huawei.agconnect:agcp:1.9.1.304") + } + } + } +} + +dependencyResolutionManagement { + repositories { + maven(url = "https://developer.huawei.com/repo") + } +} +``` + +## Configuration + +### AndroidManifest.xml + +Add your Project ID and API Key inside ``: + +```xml + + + +``` + +### Application initialization + +Initialize the SDK in your `Application` class. + +The SDK requires WorkManager to be initialized first. The standard WorkManager setup does this +automatically through AndroidX Startup. If your application disables WorkManager's automatic +initializer, initialize WorkManager manually before calling `PushNotifications.initialize(...)`. + +#### Automatic (from AndroidManifest.xml) + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + PushNotifications.initialize(this) + } +} +``` + +#### Manual + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + PushNotifications.initialize( + application = this, + config = Config.create( + projectId = "your-project-id", + apiKey = "your-api-key", + isDebug = true + ) + ) + } +} +``` + +#### Switching to another project + +Changing manifest credentials is not a supported project-migration mechanism. +To switch an explicitly configured SDK to another project, deinitialize the +current runtime and wait for that operation to complete before initializing the +new one: + +```kotlin +PushNotifications.deinitialize() +PushNotifications.initialize( + application = application, + config = newConfig, +) +``` + +Java callers can chain the asynchronous wrapper: + +```java +PushNotifications.deinitializeAsync() + .thenRun(() -> PushNotifications.initialize(application, newConfig)); +``` + +Deinitialization removes Live Activities first, then unsubscribes the current +subscriber and clears persisted project data. If any step fails, the SDK remains +initialized and the operation throws an exception. Live Activities already +removed stay removed. + +### Notification UI customization + +You may override the following resources in your app: + +- Default notification color + `@color/pushpushgo_notification_color_default` +- Default notification channel ID + `@string/pushpushgo_notification_default_channel_id` +- Default notification channel name + `@string/pushpushgo_notification_default_channel_name` +- Small notification icon (per density): + - `res/drawable-mdpi/ic_stat_pushpushgo_default` + - `res/drawable-xhdpi/ic_stat_pushpushgo_default` + - `res/drawable-xxhdpi/ic_stat_pushpushgo_default` + +## Handling notification clicks + +To ensure correct handling of notification taps: + +1. Set your launcher activity to `singleTop`. +2. Add the following `intent-filter`. + + ```xml + + + + + + + ``` + +3. Forward the intent to the SDK in `onCreate` and `onNewIntent`. + + ```kotlin + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState == null) { + PushNotifications.handleBackgroundNotificationClick(intent) + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + PushNotifications.handleBackgroundNotificationClick(intent) + } + ``` + +This ensures notification data is processed both when the app is cold-started and when it is already running. + +## Additional integration info + +- https://pushpushgo.productfruits.help/en/article/web-mobile-push-integration + +## Basic usage + +### Push subscription + +```kotlin +PushNotifications.isSubscribed() + +PushNotifications.subscribe() +PushNotifications.unsubscribe() +``` + +From Java, use `subscribeAsync()` and `unsubscribeAsync()`. Both return a +`CompletableFuture`. + +#### Notification permission required + +On Android 13 (API 33) and newer, push subscription requires the +`POST_NOTIFICATIONS` permission to be granted by the user. + +If the permission is not granted, `subscribe()` (and its Java wrapper, +`subscribeAsync()`) throws an exception. + +The application is responsible for requesting the permission before calling +any subscription methods. + +#### Permission monitoring + +The SDK periodically checks whether the notification permission is still granted. +If the permission is revoked while the user is subscribed, the SDK automatically +unsubscribes the user. + +### Beacons, tags, and dynamic groups + +```kotlin +val beacon = + BeaconBuilder() + .set("see_invoice", true) + .setCustomId("CID") + .appendTag("demo") + .appendTag("mobile", "platform") + .assignToGroup("my-group-name") + .build() + +PushNotifications.sendBeacon(beacon) +``` + +## Live Activities + +Real-time, continuously updated notifications (Android 16+ Live Updates), e.g. +live football match tracking: + +```kotlin +val liveActivities = PushNotifications.liveActivities +liveActivities.subscribe("liveNotificationId") +// ... +liveActivities.unsubscribe("liveNotificationId") +``` + +For the full integration guide (clicks, deep links, analytics, rendering +features) see [LIVE_ACTIVITIES.md](LIVE_ACTIVITIES.md). diff --git a/library/api/library.api b/push/api/push.api similarity index 76% rename from library/api/library.api rename to push/api/push.api index 21c59e87..e180d3bf 100644 --- a/library/api/library.api +++ b/push/api/push.api @@ -1,109 +1,100 @@ -public final class com/pushpushgo/sdk/BeaconBuilder { - public final fun appendTag (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Lcom/pushpushgo/sdk/BeaconBuilder; - public static synthetic fun appendTag$default (Lcom/pushpushgo/sdk/BeaconBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun assignToGroup (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; +public final class com/pushpushgo/sdk/push/Beacon { +} + +public final class com/pushpushgo/sdk/push/BeaconBuilder { + public fun ()V + public final fun appendTag (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun appendTag (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Lcom/pushpushgo/sdk/push/BeaconTagStrategy;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun appendTag (Ljava/lang/String;Ljava/lang/String;Lcom/pushpushgo/sdk/push/BeaconTagStrategy;I)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public static synthetic fun appendTag$default (Lcom/pushpushgo/sdk/push/BeaconBuilder;Ljava/lang/String;Ljava/lang/String;Lcom/pushpushgo/sdk/push/BeaconTagStrategy;IILjava/lang/Object;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun assignToGroup (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun build ()Lcom/pushpushgo/sdk/push/Beacon; public final fun getTags ()Ljava/util/List; public final fun getTagsToDelete ()Ljava/util/List; - public final fun removeTag ([Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun removeTags (Ljava/util/List;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun removeTags (Ljava/util/Map;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun send ()V - public final fun set (Ljava/lang/String;Ljava/lang/Object;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun setCustomId (Ljava/lang/Integer;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun setCustomId (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun unassignFromGroup (Ljava/lang/String;)Lcom/pushpushgo/sdk/BeaconBuilder; + public final fun removeTag ([Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun removeTags (Ljava/util/List;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun removeTags (Ljava/util/Map;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun set (Ljava/lang/String;C)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun set (Ljava/lang/String;Ljava/lang/Number;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun set (Ljava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun set (Ljava/lang/String;Z)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun setCustomId (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; + public final fun unassignFromGroup (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconBuilder; +} + +public final class com/pushpushgo/sdk/push/BeaconTagStrategy : java/lang/Enum { + public static final field APPEND Lcom/pushpushgo/sdk/push/BeaconTagStrategy; + public static final field REWRITE Lcom/pushpushgo/sdk/push/BeaconTagStrategy; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/BeaconTagStrategy; + public static fun values ()[Lcom/pushpushgo/sdk/push/BeaconTagStrategy; } -public final class com/pushpushgo/sdk/BuildConfig { +public final class com/pushpushgo/sdk/push/BuildConfig { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEBUG Z public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; public fun ()V } -public final class com/pushpushgo/sdk/PushPushGo { - public static final field Companion Lcom/pushpushgo/sdk/PushPushGo$Companion; - public static final field VERSION Ljava/lang/String; - public synthetic fun (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun areNotificationsEnabled ()Z - public final fun createBeacon ()Lcom/pushpushgo/sdk/BeaconBuilder; - public final fun createSubscriber ()Lcom/google/common/util/concurrent/ListenableFuture; - public final fun getActiveLiveActivities ()Ljava/util/List; - public final fun getApiKey ()Ljava/lang/String; - public final fun getCustomClickIntentFlags ()I - public final fun getDefaultIsSubscribed ()Z - public static final fun getInstance ()Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;)Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;Z)Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZ)Lcom/pushpushgo/sdk/PushPushGo; - public static final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getLiveActivitySubscriberId (Ljava/lang/String;)Ljava/lang/String; - public final fun getNotificationDetails (Landroid/content/Intent;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public final fun getNotificationDetails (Ljava/util/Map;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public final fun getNotificationHandler ()Lkotlin/jvm/functions/Function3; - public final fun getOnInvalidProjectIdHandler ()Lkotlin/jvm/functions/Function3; - public final fun getProjectId ()Ljava/lang/String; - public final fun getPushToken ()Lcom/google/common/util/concurrent/ListenableFuture; - public final fun getSubscriberId ()Ljava/lang/String; - public final fun handleBackgroundNotificationClick (Landroid/content/Intent;I)V - public static synthetic fun handleBackgroundNotificationClick$default (Lcom/pushpushgo/sdk/PushPushGo;Landroid/content/Intent;IILjava/lang/Object;)V - public final fun handleLiveActivityClick (Landroid/content/Intent;)Ljava/lang/String; - public final fun handleLiveActivityClick (Landroid/content/Intent;Z)Ljava/lang/String; - public static synthetic fun handleLiveActivityClick$default (Lcom/pushpushgo/sdk/PushPushGo;Landroid/content/Intent;ZILjava/lang/Object;)Ljava/lang/String; - public final fun isLiveActivitiesSupported ()Z - public final fun isLiveActivityActive (Ljava/lang/String;)Z - public final fun isPPGoPush (Landroid/content/Intent;)Z - public final fun isPPGoPush (Ljava/util/Map;)Z - public final fun isSubscribed ()Z - public final fun migrateToNewProject (Ljava/lang/String;Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; - public final fun registerSubscriber ()V - public final fun setCustomClickIntentFlags (I)V - public final fun setDefaultIsSubscribed (Z)V - public final fun setNotificationHandler (Lkotlin/jvm/functions/Function3;)V - public final fun setOnInvalidProjectIdHandler (Lkotlin/jvm/functions/Function3;)V - public final fun simulateLiveActivityPush (Ljava/util/Map;)V - public final fun subscribeToLiveActivity (Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; - public final fun unregisterSubscriber ()V - public final fun unregisterSubscriber (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; - public final fun unsubscribeFromLiveActivity (Ljava/lang/String;)Lcom/google/common/util/concurrent/ListenableFuture; -} - -public final class com/pushpushgo/sdk/PushPushGo$Companion { - public final fun getInstance ()Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;Z)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZ)Lcom/pushpushgo/sdk/PushPushGo; - public final fun getInstance (Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;)Lcom/pushpushgo/sdk/PushPushGo; - public static synthetic fun getInstance$default (Lcom/pushpushgo/sdk/PushPushGo$Companion;Landroid/app/Application;Ljava/lang/String;Ljava/lang/String;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/pushpushgo/sdk/PushPushGo; - public final fun isInitialized ()Z -} - -public final class com/pushpushgo/sdk/bridge/PushPushGoSubscriptionBridgeManager : com/pushpushgo/sdk/bridge/PushSubscriptionBridgeManager { - public static final field Companion Lcom/pushpushgo/sdk/bridge/PushPushGoSubscriptionBridgeManager$Companion; - public fun ()V - public fun requestSubscription (Landroid/content/Context;)Z -} - -public final class com/pushpushgo/sdk/bridge/PushPushGoSubscriptionBridgeManager$Companion { +public abstract interface class com/pushpushgo/sdk/push/InvalidProjectIdHandler { + public abstract fun onInvalidProjectId (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V } -public abstract interface class com/pushpushgo/sdk/bridge/PushSubscriptionBridgeManager { - public abstract fun requestSubscription (Landroid/content/Context;)Z +public abstract interface class com/pushpushgo/sdk/push/NotificationClickHandler { + public abstract fun onNotificationClick (Landroid/content/Context;Ljava/lang/String;I)V } -public final class com/pushpushgo/sdk/dto/PPGoNotification { - public fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class com/pushpushgo/sdk/push/PushNotifications { + public static final field INSTANCE Lcom/pushpushgo/sdk/push/PushNotifications; + public static final field VERSION Ljava/lang/String; + public static final fun areNotificationsEnabled ()Z + public final synthetic fun deinitialize (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun deinitializeAsync ()Ljava/util/concurrent/CompletableFuture; + public static final fun getApiKey ()Ljava/lang/String; + public static final fun getCustomClickIntentFlags ()I + public static final fun getLiveActivities ()Lcom/pushpushgo/sdk/push/liveactivity/LiveActivities; + public static final fun getNotificationDetails (Landroid/content/Intent;)Lcom/pushpushgo/sdk/push/dto/PushPushGoNotification; + public static final fun getNotificationDetails (Ljava/util/Map;)Lcom/pushpushgo/sdk/push/dto/PushPushGoNotification; + public static final fun getProjectId ()Ljava/lang/String; + public static final fun getPushSubscriptionProvider ()Lcom/pushpushgo/sdk/core/api/PushSubscriptionProvider; + public static final fun getPushToken ()Ljava/lang/String; + public static final fun getSubscriberId ()Ljava/lang/String; + public static final fun handleBackgroundNotificationClick (Landroid/content/Intent;I)V + public static synthetic fun handleBackgroundNotificationClick$default (Landroid/content/Intent;IILjava/lang/Object;)V + public static final fun initialize (Landroid/app/Application;)Lcom/pushpushgo/sdk/push/PushNotifications; + public static final fun initialize (Landroid/app/Application;Lcom/pushpushgo/sdk/core/api/Config;)Lcom/pushpushgo/sdk/push/PushNotifications; + public static final fun isInitialized ()Z + public static final fun isPushPushGoNotification (Landroid/content/Intent;)Z + public static final fun isPushPushGoNotification (Ljava/util/Map;)Z + public static final fun isSubscribed ()Z + public static final synthetic fun sendBeacon (Lcom/pushpushgo/sdk/push/Beacon;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun sendBeaconAsync (Lcom/pushpushgo/sdk/push/Beacon;)Ljava/util/concurrent/CompletableFuture; + public static final fun setCustomClickIntentFlags (I)V + public static final fun setErrorCallback (Lcom/pushpushgo/sdk/push/PushNotificationsErrorCallback;)V + public static final fun setInvalidProjectIdHandler (Lcom/pushpushgo/sdk/push/InvalidProjectIdHandler;)V + public static final fun setNotificationClickHandler (Lcom/pushpushgo/sdk/push/NotificationClickHandler;)V + public final synthetic fun subscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun subscribeAsync ()Ljava/util/concurrent/CompletableFuture; + public final synthetic fun unsubscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun unsubscribeAsync ()Ljava/util/concurrent/CompletableFuture; +} + +public abstract interface class com/pushpushgo/sdk/push/PushNotificationsErrorCallback { + public abstract fun onError (Ljava/lang/Throwable;)V +} + +public final class com/pushpushgo/sdk/push/dto/PushPushGoNotification { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; - public final fun component3 ()I + public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)Lcom/pushpushgo/sdk/dto/PPGoNotification; - public static synthetic fun copy$default (Lcom/pushpushgo/sdk/dto/PPGoNotification;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/pushpushgo/sdk/dto/PPGoNotification; + public final fun component5 ()I + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Lcom/pushpushgo/sdk/push/dto/PushPushGoNotification; + public static synthetic fun copy$default (Lcom/pushpushgo/sdk/push/dto/PushPushGoNotification;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/Object;)Lcom/pushpushgo/sdk/push/dto/PushPushGoNotification; public fun equals (Ljava/lang/Object;)Z public final fun getBody ()Ljava/lang/String; public final fun getCampaignId ()Ljava/lang/String; @@ -114,7 +105,22 @@ public final class com/pushpushgo/sdk/dto/PPGoNotification { public fun toString ()Ljava/lang/String; } -public final class com/pushpushgo/sdk/exception/PushPushException : java/io/IOException { +public final class com/pushpushgo/sdk/push/exception/PushPushException : java/io/IOException { +} + +public final class com/pushpushgo/sdk/push/liveactivity/LiveActivities { + public final fun getActiveActivities ()Ljava/util/List; + public final fun getSubscriberId (Ljava/lang/String;)Ljava/lang/String; + public final fun handleClick (Landroid/content/Intent;)Ljava/lang/String; + public final fun handleClick (Landroid/content/Intent;Z)Ljava/lang/String; + public static synthetic fun handleClick$default (Lcom/pushpushgo/sdk/push/liveactivity/LiveActivities;Landroid/content/Intent;ZILjava/lang/Object;)Ljava/lang/String; + public final fun isActive (Ljava/lang/String;)Z + public final fun isSupported ()Z + public final fun simulatePush (Ljava/util/Map;)V + public final synthetic fun subscribe (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun subscribeAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; + public final synthetic fun unsubscribe (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun unsubscribeAsync (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; } public final class com/pushpushgo/sdk/push/liveactivity/LiveActivityDismissReceiver : android/content/BroadcastReceiver { @@ -390,17 +396,25 @@ public final class com/pushpushgo/sdk/push/liveactivity/data/MatchPhase$Companio public final fun fromValue (Ljava/lang/String;)Lcom/pushpushgo/sdk/push/liveactivity/data/MatchPhase; } -public final class com/pushpushgo/sdk/push/service/FcmMessagingServiceDelegate { +public final class com/pushpushgo/sdk/push/push/service/FcmMessagingServiceDelegate { public fun (Landroid/content/Context;)V public final fun onDestroy ()V public final fun onMessageReceived (Lcom/google/firebase/messaging/RemoteMessage;)V public final fun onNewToken (Ljava/lang/String;)V } -public final class com/pushpushgo/sdk/push/service/HmsMessagingServiceDelegate { +public final class com/pushpushgo/sdk/push/push/service/HmsMessagingServiceDelegate { public fun (Landroid/content/Context;)V public final fun onDestroy ()V public final fun onMessageReceived (Lcom/huawei/hms/push/RemoteMessage;)V public final fun onNewToken (Ljava/lang/String;)V } +public final class com/pushpushgo/sdk/push/subscription/DefaultPushSubscriptionProvider : com/pushpushgo/sdk/core/api/PushSubscriptionProvider { + public fun getPushToken ()Ljava/lang/String; + public fun isNotificationChannelEnabled ()Z + public fun isSubscribed ()Z + public fun subscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun unsubscribe (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + diff --git a/push/build.gradle.kts b/push/build.gradle.kts new file mode 100644 index 00000000..0a23d3ff --- /dev/null +++ b/push/build.gradle.kts @@ -0,0 +1,119 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.SourcesJar +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.binary.validator) + alias(libs.plugins.ktlint) + alias(libs.plugins.maven.publish) +} + +group = "com.pushpushgo" +version = + requireNotNull(property("VERSION")) { + "VERSION property must be defined" + }.toString() + +android { + namespace = "com.pushpushgo.sdk.push" + compileSdk = 36 + + defaultConfig { + minSdk = 26 + + consumerProguardFiles("consumer-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + buildFeatures { + buildConfig = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + languageVersion.set(KotlinVersion.KOTLIN_2_1) + apiVersion.set(KotlinVersion.KOTLIN_2_1) + } + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } +} + +dependencies { + api(project(":core")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.preference) + + implementation(libs.coroutines.core) + implementation(libs.coroutines.android) + + implementation(libs.retrofit) + implementation(libs.retrofit.moshi) + + implementation(platform(libs.okhttp.bom)) + implementation(libs.okhttp.logging) + + ksp(libs.moshi.codegen) + implementation(libs.moshi.kotlin) + implementation(libs.moshi.adapters) + + implementation(libs.androidx.work.runtime) + implementation(libs.androidx.work.gcm) + + compileOnly(platform(libs.firebase.bom)) + compileOnly(libs.firebase.messaging) + + compileOnly(libs.hms.push) + + testImplementation(libs.junit) + testImplementation(libs.mockk) + testImplementation(libs.json) + testImplementation(libs.androidx.test.junit) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.work.testing) + + testImplementation(platform(libs.firebase.bom)) + testImplementation(libs.firebase.messaging) +} + +apiValidation { + ignoredProjects.addAll(listOf("firebase", "hms", "java")) +} + +mavenPublishing { + coordinates(group.toString(), "sdk-push", version.toString()) + + pom { + name.set("PushPushGo PushNotifications SDK") + } + + configure( + AndroidSingleVariantLibrary( + javadocJar = JavadocJar.Empty(), + sourcesJar = SourcesJar.Sources(), + variant = "release", + ), + ) +} diff --git a/push/consumer-rules.pro b/push/consumer-rules.pro new file mode 100644 index 00000000..026ce0e1 --- /dev/null +++ b/push/consumer-rules.pro @@ -0,0 +1,10 @@ +-keep,allowoptimization,allowobfuscation class com.google.firebase.messaging.FirebaseMessaging +-keep,allowoptimization,allowobfuscation class com.huawei.hms.aaid.HmsInstanceId +-keep,allowoptimization,allowobfuscation class com.huawei.agconnect.AGConnectOptionsBuilder + +-dontwarn com.google.firebase.messaging.FirebaseMessaging +-dontwarn com.google.firebase.messaging.FirebaseMessagingService +-dontwarn com.huawei.agconnect.AGConnectOptions +-dontwarn com.huawei.agconnect.AGConnectOptionsBuilder +-dontwarn com.huawei.hms.aaid.HmsInstanceId +-dontwarn com.huawei.hms.push.HmsMessageService diff --git a/push/gradle.properties b/push/gradle.properties new file mode 100644 index 00000000..275a949c --- /dev/null +++ b/push/gradle.properties @@ -0,0 +1 @@ +VERSION=4.0.0-SNAPSHOT diff --git a/sample/.gitignore b/push/sample/firebase/.gitignore similarity index 100% rename from sample/.gitignore rename to push/sample/firebase/.gitignore diff --git a/push/sample/firebase/build.gradle.kts b/push/sample/firebase/build.gradle.kts new file mode 100644 index 00000000..9d667900 --- /dev/null +++ b/push/sample/firebase/build.gradle.kts @@ -0,0 +1,55 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.google.services) + alias(libs.plugins.ktlint) +} + +android { + namespace = "com.pushpushgo.sdk.sample.push.firebase" + compileSdk = 36 + + defaultConfig { + minSdk = 28 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +dependencies { + implementation(project(":push")) + + implementation(libs.timber) + + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.lifecycle.runtime) + + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.messaging) +} diff --git a/sample/google-services.json b/push/sample/firebase/google-services.json similarity index 93% rename from sample/google-services.json rename to push/sample/firebase/google-services.json index 7c5fa255..53f7dec9 100644 --- a/sample/google-services.json +++ b/push/sample/firebase/google-services.json @@ -10,7 +10,7 @@ "client_info": { "mobilesdk_app_id": "1:120314134586:android:01615cfde51b13c2393503", "android_client_info": { - "package_name": "com.pushpushgo.sample" + "package_name": "com.pushpushgo.sdk.sample.push.firebase" } }, "oauth_client": [ @@ -37,4 +37,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/library-inappmessages/consumer-rules.pro b/push/sample/firebase/proguard-rules.pro similarity index 100% rename from library-inappmessages/consumer-rules.pro rename to push/sample/firebase/proguard-rules.pro diff --git a/sample/src/main/AndroidManifest.xml b/push/sample/firebase/src/main/AndroidManifest.xml similarity index 100% rename from sample/src/main/AndroidManifest.xml rename to push/sample/firebase/src/main/AndroidManifest.xml diff --git a/sample/src/main/ic_launcher-playstore.png b/push/sample/firebase/src/main/ic_launcher-playstore.png similarity index 100% rename from sample/src/main/ic_launcher-playstore.png rename to push/sample/firebase/src/main/ic_launcher-playstore.png diff --git a/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/MainApplication.kt b/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/MainApplication.kt new file mode 100644 index 00000000..3f8e0703 --- /dev/null +++ b/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/MainApplication.kt @@ -0,0 +1,52 @@ +package com.pushpushgo.sdk.sample.push.firebase + +import android.app.Application +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import com.pushpushgo.sdk.push.PushNotifications +import com.pushpushgo.sdk.sample.push.firebase.activity.BeaconActivity +import com.pushpushgo.sdk.sample.push.firebase.activity.LiveActivityDemoActivity +import timber.log.Timber + +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + + Timber.plant(Timber.DebugTree()) + + PushNotifications + .initialize(this) + .apply { + setCustomClickIntentFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + // Single link-routing point for regular push redirectLinks AND Live + // Activity deep links: app:/// navigates in-app, anything + // else (https etc.) resolves via the system. + setNotificationClickHandler { _, url, flags -> routeLink(url, flags) } + } + } + + private fun routeLink( + url: String, + flags: Int, + ) { + Timber.tag("PPGO_SAMPLE").d("routeLink: $url") + val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return + + val intent = + when { + uri.scheme == "app" && uri.pathSegments.firstOrNull()?.lowercase() == "beacons" -> + Intent(this, BeaconActivity::class.java) + uri.scheme == "app" && uri.pathSegments.firstOrNull()?.lowercase() in setOf("live-activities", "liveactivities") -> + Intent(this, LiveActivityDemoActivity::class.java) + else -> Intent(Intent.ACTION_VIEW, uri) + } + intent.addFlags(flags or Intent.FLAG_ACTIVITY_NEW_TASK) + + runCatching { startActivity(intent) } + .onFailure { + Timber.tag("PPGO_SAMPLE").e(it, "No activity for link: $url") + Toast.makeText(this, "Cannot open: $url", Toast.LENGTH_SHORT).show() + } + } +} diff --git a/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/activity/BeaconActivity.kt b/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/activity/BeaconActivity.kt new file mode 100644 index 00000000..992d467e --- /dev/null +++ b/push/sample/firebase/src/main/java/com/pushpushgo/sdk/sample/push/firebase/activity/BeaconActivity.kt @@ -0,0 +1,119 @@ +package com.pushpushgo.sdk.sample.push.firebase.activity + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.pushpushgo.sdk.push.Beacon +import com.pushpushgo.sdk.push.BeaconBuilder +import com.pushpushgo.sdk.push.PushNotifications +import com.pushpushgo.sdk.sample.push.firebase.R +import kotlinx.coroutines.launch +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Date + +class BeaconActivity : AppCompatActivity(R.layout.activity_beacon) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Timber.plant( + object : Timber.Tree() { + @SuppressLint("SetTextI18n", "SimpleDateFormat") + override fun log( + priority: Int, + tag: String?, + message: String, + t: Throwable?, + ) { + if (priority > Log.VERBOSE) { + with(findViewById(R.id.logs)) { + post { text = "${SimpleDateFormat("HH:mm:ss").format(Date())}: $message\n$text" } + } + } + } + }, + ) + + findViewById