Blog Infos
Author
Published
Topics
, , , ,
Published

This article is a continuation of the my article about the implementation of the in-app review logic in the KMP module. Previous implementation (version 1.x of the library) supported App GalleryGoogle Play and App Store. In version 2.0 I implemented the support of the Galaxy StoreRuStore and Amazon App Store.

To support the new markets I had to refactor the modules structure a bit. Since Google Play and RuStore require to add the SDK to launch the in-app review flow I decided to put them into the separate modules. It allows to avoid adding unnecessary dependencies if you want to use only App Gallery for example (that doesn’t need any SDK). So, now the library can be added in three ways:

implementation("com.mikhailovskii.kmp:in-app-review-kmp:$latest_tag") // Amazon, App Gallery, Galaxy Store
implementation("com.mikhailovskii.kmp:in-app-review-kmp-google-play:$latest_tag") // Google Play + Amazon, App Gallery, Galaxy Store
implementation("com.mikhailovskii.kmp:in-app-review-kmp-rustore:$latest_tag") // RuStore + Amazon, App Gallery, Galaxy Store
view raw dependencies.kt hosted with ❤ by GitHub

That’s all about the refactoring and let’s move to the implementation of the new markets part.

Amazon App Store

This market doesn’t provide any API to launch in-app review or open the comments section in market so in this case we just open the app’s page in market. Here’s the way how it is implemented:

class AmazonInAppReviewInitParams(val context: Context)
class AmazonInAppReviewManager(
private val params: AmazonInAppReviewInitParams
) : InAppReviewDelegate {
override fun requestInAppReview() = flow {
val context = params.context
context.openMarket(
deeplink = "amzn://apps/android?p=${context.packageName}",
url = "https://www.amazon.com/gp/mas/dl/android?p=${context.packageName}"
)
emit(ReviewCode.NO_ERROR)
}
override fun requestInMarketReview() = requestInAppReview()
}

As you can see, the setup is pretty easy. For the convenience I created the extension for opening the market, that’s tries to launch the deeplink and if the deeplink cannot be opened it opens the URL.

fun Context.openMarket(deeplink: String, url: String) {
val marketAppIntent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplink)).apply {
flags += Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
}
val marketInBrowserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
runCatching {
startActivity(marketAppIntent)
}.getOrElse {
startActivity(marketInBrowserIntent)
}
}
view raw Context.kt hosted with ❤ by GitHub

After the Amazon App Store/Browser with the app’s page is opened the library emits the state that everything is finished ok.

Galaxy Store

Its implementation is more complicated. This is a source code of it and I’ll comment it lower:

class GalaxyStoreInAppReviewInitParams(val context: Context)
class GalaxyStoreInAppReviewManager(
private val params: GalaxyStoreInAppReviewInitParams
) : InAppReviewDelegate {
private var deepLinkUri: String? = null
private var errorCode: Int = 0
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun init() {
super.init()
val context = params.context
val filter = IntentFilter("com.sec.android.app.samsungapps.RESPONSE_INAPP_REVIEW_AUTHORITY")
val authorityReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val hasAuthority = intent.getBooleanExtra("hasAuthority", false)
errorCode = intent.getIntExtra("errorCode", 0)
if (hasAuthority) {
deepLinkUri = intent.getStringExtra("deeplinkUri")
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(authorityReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(authorityReceiver, filter)
}
val intent = Intent("com.sec.android.app.samsungapps.REQUEST_INAPP_REVIEW_AUTHORITY").apply {
`package` = "com.sec.android.app.samsungapps"
putExtra("callerPackage", context.packageName)
}
context.sendBroadcast(intent)
}
override fun requestInAppReview() = flow {
if (errorCode != 0) {
emit(ReviewCode.fromCode(errorCode))
} else {
deepLinkUri?.let {
val intent = Intent().apply {
data = Uri.parse(it)
flags += Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_INCLUDE_STOPPED_PACKAGES
}
params.context.startActivity(intent)
emit(ReviewCode.NO_ERROR)
} ?: run {
emit(ReviewCode.INTERNAL_ERROR)
}
}
}
override fun requestInMarketReview() = flow {
val context = params.context
val packageName = context.packageName
context.openMarket(
deeplink = "samsungapps://AppRating/$packageName",
url = "https://apps.samsung.com/appquery/AppRating.as?appId=$packageName"
)
emit(ReviewCode.NO_ERROR)
}
}

So, first of all, you can notice that this method overrides the init method. We also needed to do it for the App Gallery (to register the result listeners), but let’s see, why do we need to do it in this case. The reason is pretty straightforward — to launch the in-app review we need the deeplink and as it is said in the documentation we need to wait for some time before it is generated so it’s better to do it in advance. So, the flow is the following — we register the receiver (line 15) and send the intent to the Galaxy Store that should generate the deeplink and pass it to the receiver via the Intent. When the intent is received we check that it has authority and has no errors and if both conditions are true we get the deeplink from the bundle (lines 17–21).

Now let’s consider two cases — user launches in-app review when the deeplink is received (no errors and has authority) or the deeplink is not received (due to some error). Let’s start from the first case — in this case we launch the deeplink and emit the success state into the flow (lines 41–46). In the second case we emit the error (if we know the error code we try to map it into the library errors enum or just emit the general error). Galaxy Store errors are described in the documentation.

This is an example how in-app review pop-up from the Galaxy Store looks like:

Image is taken from the documentation

 

To launch in-market review we use the same pattern as we do with any other store — by default try to open the activity via deeplink and in case of failure open it in browser (lines 56–59). Here’s how this activity looks like:

Image is taken from the documentation

 

RuStore

The last but not the least. RuStore implementation requires an additional SDK and that’s why was implemented as a separate module of the library. To add the SDK I had to add a new repository into repositories block in settings.gradle.kts:

maven("https://artifactory-external.vkpartner.ru/artifactory/maven")

Be careful — in their official documentation there’s a typo in the url — single slash after https that cost me some time to understand why the SDK is not fetched 🙂

Here’s the implementation of the RuStoreInAppReviewManager:

class RuStoreInAppReviewInitParams(val context: Context)
class RuStoreInAppReviewManager(private val params: RuStoreInAppReviewInitParams) : InAppReviewDelegate {
override fun requestInAppReview() = flow {
val activity = params.context
val manager = RuStoreReviewManagerFactory.create(activity)
val reviewInfo = manager.requestReviewFlow().await()
manager.launchReviewFlow(reviewInfo).await()
emit(ReviewCode.NO_ERROR)
}.catch { e ->
if (e is RuStoreException) {
val exceptionMapper = RuStoreExceptionMapper()
emit(exceptionMapper(e))
} else {
throw e
}
}
override fun requestInMarketReview() = flow {
val context = params.context
val packageName = context.packageName
context.openMarket(
deeplink = "rustore://apps.rustore.ru/app/$packageName",
url = "https://apps.rustore.ru/app/$packageName"
)
emit(ReviewCode.NO_ERROR)
}
}

Logic is the same as in Google Play’s implementation (that you could see in the previous article. But it’s important to highlight the error handling — for this purpose I created a separate mapper:

internal class RuStoreExceptionMapper {
operator fun invoke(throwable: RuStoreException) = when (throwable) {
is RuStoreNotInstalledException -> ReviewCode.STORE_NOT_FOUND
is RuStoreOutdatedException -> ReviewCode.STORE_OUTDATED
is RuStoreUserBannedException -> ReviewCode.USER_BANNED
is RuStoreApplicationBannedException -> ReviewCode.APP_BANNED
is RuStoreRequestLimitReached -> ReviewCode.REQUEST_LIMIT_REACHED
is RuStoreReviewExists -> ReviewCode.REVIEW_EXISTS
is RuStoreInvalidReviewInfo -> ReviewCode.INVALID_REVIEW_INFO
else -> throw throwable
}
}

All the exceptions are subclasses of the RuStoreException and you can understand more accurate reason of the exception (i.e. market not installed/outdated and so on). This is how RuStore’s in-app review pop-up looks like:

Image is taken from the documentation

Here’s the full code of the project.

Now the library covers more markets but if there’s no market you need feel free to create an issue/the PR 🙂

Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.

This article is previously published on proandroiddev.com.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu