From 98933877929beb056ec98469ecbfbb8618c1c780 Mon Sep 17 00:00:00 2001 From: dariakharlan Date: Thu, 27 Aug 2026 17:55:21 +0400 Subject: [PATCH] feat: AppsFlyer unified deep linking to registration screen Adds AppsFlyer Android SDK integration for Unified Deep Linking (UDL), following the existing Branch integration pattern. Deep links from go.proanalytics.academy, proanalytics.onelink.me, and the academyapp:// scheme resolve via AppsFlyerDeepLinkHandler and route to the registration (sign-up) screen by default, unless the OneLink template overrides the destination via a screen_name parameter. - New AppsFlyerConfig / Config.getAppsFlyerConfig(), gated by an APPSFLYER.ENABLED flag in config.yaml (mirrors BRANCH config) - Manifest intent-filters for the custom scheme and App Links domains - New DeepLinkType.REGISTER wired into DeepLinkRouter, respecting REGISTRATION_ENABLED and current login state - AppsFlyerDeepLinkHandler subscribes to the SDK's DeepLinkListener and exposes a replay-1 SharedFlow consumed by AppActivity, avoiding races between SDK resolution and Activity startup Co-Authored-By: Claude Sonnet 5 --- app/src/main/AndroidManifest.xml | 23 +++++++ .../main/java/org/openedx/app/AppActivity.kt | 11 ++++ .../main/java/org/openedx/app/OpenEdXApp.kt | 6 ++ .../app/deeplink/AppsFlyerDeepLinkHandler.kt | 60 +++++++++++++++++++ .../java/org/openedx/app/deeplink/DeepLink.kt | 1 + .../openedx/app/deeplink/DeepLinkRouter.kt | 16 +++++ .../main/java/org/openedx/app/di/AppModule.kt | 2 + build.gradle | 1 + core/build.gradle | 3 + .../openedx/core/config/AppsFlyerConfig.kt | 11 ++++ .../java/org/openedx/core/config/Config.kt | 5 ++ default_config/dev/config.yaml | 4 ++ default_config/prod/config.yaml | 4 ++ default_config/stage/config.yaml | 4 ++ 14 files changed, 151 insertions(+) create mode 100644 app/src/main/java/org/openedx/app/deeplink/AppsFlyerDeepLinkHandler.kt create mode 100644 core/src/main/java/org/openedx/core/config/AppsFlyerConfig.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65c64e538..d90b3f13e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ android:screenOrientation="sensor" android:supportsRtl="true" android:theme="@style/Theme.App.Starting" + tools:replace="android:allowBackup,android:dataExtractionRules,android:fullBackupContent" tools:targetApi="tiramisu"> + + + + + + + + + + + + + + + + + + + + + + () private val downloadDialogManager by inject() private val calendarSyncScheduler by inject() + private val appsFlyerDeepLinkHandler by inject() private val branchLogger = Logger(BRANCH_TAG) @@ -114,6 +116,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { setupInitialFragment(savedInstanceState) observeLogoutEvent() observeDownloadFailedDialog() + observeAppsFlyerDeepLink() calendarSyncScheduler.scheduleDailySync() } @@ -208,6 +211,14 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { } } + private fun observeAppsFlyerDeepLink() { + lifecycleScope.launch { + appsFlyerDeepLinkHandler.deepLink.collect { deepLink -> + viewModel.makeExternalRoute(supportFragmentManager, deepLink) + } + } + } + override fun onStart() { super.onStart() diff --git a/app/src/main/java/org/openedx/app/OpenEdXApp.kt b/app/src/main/java/org/openedx/app/OpenEdXApp.kt index 6524cde5d..ad0599e2f 100644 --- a/app/src/main/java/org/openedx/app/OpenEdXApp.kt +++ b/app/src/main/java/org/openedx/app/OpenEdXApp.kt @@ -9,6 +9,7 @@ import io.branch.referral.Branch import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin +import org.openedx.app.deeplink.AppsFlyerDeepLinkHandler import org.openedx.app.deeplink.BranchBrazeDeeplinkHandler import org.openedx.app.di.appModule import org.openedx.app.di.networkingModule @@ -20,6 +21,7 @@ class OpenEdXApp : Application() { private val config by inject() private val pluginManager by inject() + private val appsFlyerDeepLinkHandler by inject() override fun onCreate() { super.onCreate() @@ -44,6 +46,10 @@ class OpenEdXApp : Application() { Branch.getAutoInstance(this) } + if (config.getAppsFlyerConfig().enabled) { + appsFlyerDeepLinkHandler.init(this, config.getAppsFlyerConfig().devKey) + } + if (config.getBrazeConfig().isEnabled && config.getFirebaseConfig().enabled) { val isCloudMessagingEnabled = config.getFirebaseConfig().isCloudMessagingEnabled && config.getBrazeConfig().isPushNotificationsEnabled diff --git a/app/src/main/java/org/openedx/app/deeplink/AppsFlyerDeepLinkHandler.kt b/app/src/main/java/org/openedx/app/deeplink/AppsFlyerDeepLinkHandler.kt new file mode 100644 index 000000000..0a369ee6d --- /dev/null +++ b/app/src/main/java/org/openedx/app/deeplink/AppsFlyerDeepLinkHandler.kt @@ -0,0 +1,60 @@ +package org.openedx.app.deeplink + +import android.content.Context +import com.appsflyer.AppsFlyerLib +import com.appsflyer.deeplink.DeepLinkListener +import com.appsflyer.deeplink.DeepLinkResult +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import org.json.JSONObject +import org.openedx.core.utils.Logger + +class AppsFlyerDeepLinkHandler { + + private val logger = Logger(TAG) + + private val _deepLink = MutableSharedFlow(replay = 1, extraBufferCapacity = 1) + val deepLink: SharedFlow = _deepLink.asSharedFlow() + + fun init(context: Context, devKey: String) { + val appsFlyerLib = AppsFlyerLib.getInstance() + appsFlyerLib.subscribeForDeepLink(object : DeepLinkListener { + override fun onDeepLinking(deepLinkResult: DeepLinkResult) { + when (deepLinkResult.status) { + DeepLinkResult.Status.FOUND -> { + logger.i { "AppsFlyer deep link found." } + _deepLink.tryEmit(DeepLink(buildParams(deepLinkResult))) + } + + DeepLinkResult.Status.NOT_FOUND -> { + logger.i { "AppsFlyer deep link not found." } + } + + DeepLinkResult.Status.ERROR -> { + logger.e { "AppsFlyer deep link resolution failed. Caused by - ${deepLinkResult.error}" } + } + } + } + }) + appsFlyerLib.init(devKey, null, context) + appsFlyerLib.start(context) + } + + // Defaults every OneLink click to the registration screen unless the OneLink + // template explicitly sets a `screen_name` deep link parameter. + private fun buildParams(deepLinkResult: DeepLinkResult): Map { + val clickEvent: JSONObject? = deepLinkResult.deepLink?.clickEvent + val params = mutableMapOf() + clickEvent?.keys()?.forEach { key -> params[key] = clickEvent.optString(key) } + + if (!params.containsKey(DeepLink.Keys.SCREEN_NAME.value)) { + params[DeepLink.Keys.SCREEN_NAME.value] = DeepLinkType.REGISTER.type + } + return params + } + + companion object { + private const val TAG = "AppsFlyer" + } +} diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt index ac494df06..0cff34d3c 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLink.kt @@ -49,6 +49,7 @@ enum class DeepLinkType(val type: String) { REMOVE_BETA_TESTER("remove_beta_tester"), FORUM_RESPONSE("forum_response"), FORUM_COMMENT("forum_comment"), + REGISTER("register"), NONE(""); companion object { diff --git a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt index 2192a6b89..befa6fe83 100644 --- a/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt +++ b/app/src/main/java/org/openedx/app/deeplink/DeepLinkRouter.kt @@ -8,6 +8,7 @@ import org.openedx.app.AppRouter import org.openedx.app.MainFragment import org.openedx.app.R import org.openedx.auth.presentation.signin.SignInFragment +import org.openedx.auth.presentation.signup.SignUpFragment import org.openedx.core.FragmentViewType import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences @@ -41,6 +42,7 @@ class DeepLinkRouter( DeepLinkType.DISCOVERY -> navigateToDiscoveryScreen(fm) DeepLinkType.DISCOVERY_COURSE_DETAIL -> navigateToCourseDetail(fm, deepLink) DeepLinkType.DISCOVERY_PROGRAM_DETAIL -> navigateToProgramDetail(fm, deepLink) + DeepLinkType.REGISTER -> navigateToRegistration(fm) else -> handleLoggedOutOrUserNavigation(fm, deepLink) } } @@ -192,6 +194,20 @@ class DeepLinkRouter( } } + private fun navigateToRegistration(fm: FragmentManager) { + when { + isUserLoggedIn -> navigateToDashboard(fm) + !config.isRegistrationEnabled() -> navigateToSignIn(fm) + appRouter.getVisibleFragment(fm = fm) !is SignUpFragment -> { + appRouter.navigateToSignUp( + fm = fm, + courseId = null, + infoType = null + ) + } + } + } + private fun navigateToCourseDashboard( fm: FragmentManager, deepLink: DeepLink, diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index cdb240387..36d4a5cbf 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -16,6 +16,7 @@ import org.openedx.app.AppRouter import org.openedx.app.BuildConfig import org.openedx.app.PluginManager import org.openedx.app.data.storage.PreferencesManager +import org.openedx.app.deeplink.AppsFlyerDeepLinkHandler import org.openedx.app.deeplink.DeepLinkRouter import org.openedx.app.room.AppDatabase import org.openedx.app.room.DATABASE_NAME @@ -129,6 +130,7 @@ val appModule = module { single { get() } single { get() } single { DeepLinkRouter(get(), get(), get(), get(), get(), get()) } + single { AppsFlyerDeepLinkHandler() } single { get() } single { get() } diff --git a/build.gradle b/build.gradle index 674a1057f..ef822abef 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ buildscript { // Third-party library versions branch_sdk_version = '5.20.0' + appsflyer_sdk_version = '6.17.4' play_services_ads_identifier_version = '18.2.0' install_referrer_version = '2.2' snakeyaml_version = '2.4' diff --git a/core/build.gradle b/core/build.gradle index 19be1f57a..9391645c7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -114,6 +114,9 @@ dependencies { api "com.google.android.gms:play-services-ads-identifier:$play_services_ads_identifier_version" api "com.android.installreferrer:installreferrer:$install_referrer_version" + // AppsFlyer SDK Integration + api "com.appsflyer:af-android-sdk:$appsflyer_sdk_version" + // Zip api "net.lingala.zip4j:zip4j:$zip_version" diff --git a/core/src/main/java/org/openedx/core/config/AppsFlyerConfig.kt b/core/src/main/java/org/openedx/core/config/AppsFlyerConfig.kt new file mode 100644 index 000000000..cc3304d96 --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/AppsFlyerConfig.kt @@ -0,0 +1,11 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class AppsFlyerConfig( + @SerializedName("ENABLED") + val enabled: Boolean = false, + + @SerializedName("DEV_KEY") + val devKey: String = "", +) diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index d26741699..1fe858151 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -100,6 +100,10 @@ class Config(context: Context) { return getObjectOrNewInstance(BRANCH, BranchConfig::class.java) } + fun getAppsFlyerConfig(): AppsFlyerConfig { + return getObjectOrNewInstance(APPSFLYER, AppsFlyerConfig::class.java) + } + fun isWhatsNewEnabled(): Boolean { return getBoolean(WHATS_NEW_ENABLED, false) } @@ -189,6 +193,7 @@ class Config(context: Context) { private const val DASHBOARD = "DASHBOARD" private const val EXPERIMENTAL_FEATURES = "EXPERIMENTAL_FEATURES" private const val BRANCH = "BRANCH" + private const val APPSFLYER = "APPSFLYER" private const val UI_COMPONENTS = "UI_COMPONENTS" private const val PLATFORM_NAME = "PLATFORM_NAME" } diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 952e041de..1ac8e76b3 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -64,6 +64,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +APPSFLYER: + ENABLED: false + DEV_KEY: '' + EXPERIMENTAL_FEATURES: APP_LEVEL_DOWNLOADS: ENABLED: false diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index a7f265a45..75bee4416 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -64,6 +64,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +APPSFLYER: + ENABLED: false + DEV_KEY: '' + EXPERIMENTAL_FEATURES: APP_LEVEL_DOWNLOADS: ENABLED: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index a7f265a45..75bee4416 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -64,6 +64,10 @@ BRANCH: HOST: '' ALTERNATE_HOST: '' +APPSFLYER: + ENABLED: false + DEV_KEY: '' + EXPERIMENTAL_FEATURES: APP_LEVEL_DOWNLOADS: ENABLED: false