{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Several months ago, Sony rolled out their all-new PlayStation Plus service, which is home to a wealth of popular classic games. Its official blog wrote that its games catalog "will continue to refresh and evolve over time, so there is always something new to play."
I was totally on board with the idea and so… I thought why not build a lightweight mobile game together with my friends and launch it on a niche app store as a pilot. I did just this. The multiplayer survival game draws on a dark cartoon style and users need to utilize their strategic skills to survive. The game launch was all about sharing ideas, among English users specifically, but it attracted many players from non-English speaking countries like China and Germany. What a surprise!
Like many other game developers, I tried to achieve monetization through in-app user purchases. The app offers many in-game props, such as fancy clothes and accessories, weapons, and skill cards, to deliver a more immersive experience or to help users survive. This posed a significant challenge — as users are based in a number of different countries or regions, the app needs to show product information in the language of the country or region where the user's account is located, as well as the currency. How to do this?
Below is a walkthrough of how I implemented the language and currency localization function and the product purchase function for my app. I turned to HMS Core In-App Purchases (IAP) because it is very accessible. I hope this will help you.
Development ProcedureProduct ManagementCreating In-App ProductsI signed in to AppGallery Connect to enable the IAP service and set relevant parameters first. After configuring the key event notification recipient address for the service, I could create products by selecting my app and going to Operate > Products > Product Management.
IAP supports three types of products, that is, consumables, non-consumables, and subscriptions. For consumables that are depleted as they are used and are repurchasable, I created products including in-game currencies (coins or gems) and items (clothes and accessories). For non-consumables that are purchased once and will never expire, I created products that unlock special game levels or characters for my app. For subscriptions, I went with products such as a monthly game membership to charge users on a recurring basis until they decide to cancel them.
Aside from selecting the product type, I also needed to set the product ID, name, language, and price, and fill in the product description. Voilà. That's how I created the in-app products.
Global Adaptation of Product InformationHere's a good thing about IAP: developers don't need to manage multiple app versions for users from different countries or regions!
All I have to do is complete the multilingual settings of the products in AppGallery Connect. First, select the product languages based on the countries/regions the product is available in. Let's say English and Chinese, in this case. Then, fill in the product information in these two languages. The effect is roughly like this:
LanguageEnglishChineseProduct nameStealth skill card隐身技能卡Product descriptionHelps a user to be invisible so that they can outsurvive their enemies.帮助用户在紧急情况下隐身,打败敌人。
Now it's time to set the product price. I only need to set the price for one country/region and then IAP will automatically adjust the local price based on the exchange rate.
After the price is set, go to the product list page and click Activate. And that's it. The product has been adapted to different locations.
Purchase ImplementationChecking Support for IAPBefore using this kit, send an isEnvReady request to HMS Core (APK) to check whether my HUAWEI ID is located in the country/region where IAP is available. According to the kit's development documentation:
If the request result is successful, my app will obtain an IsEnvReadyResult instance, indicating that the kit is supported in my location.
If the request fails, an exception object will be returned. When the object is IapApiException, use its getStatusCode method to obtain the result code of the request. If the result code is OrderStatusCode.ORDER_HWID_NOT_LOGIN (no HUAWEI ID signed in), use the getStatus method of the IapApiException object to obtain a Status object, and use the startResolutionForResult method of Status to bring up the sign-in screen. Then, obtain the result in the onActivityResult method of Activity. Parse returnCode from the intent returned by onActivityResult. If the value of returnCode is OrderStatusCode.ORDER_STATE_SUCCESS, the country/region where the currently signed-in ID is located supports IAP. Otherwise, an exception occurs.
You guys can see my coding below.
Code:
// Obtain the Activity object.
final Activity activity = getActivity();
Task<IsEnvReadyResult> task = Iap.getIapClient(activity).isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult result) {
// Obtain the execution result.
String carrierId = result.getCarrierId();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
// HUAWEI ID is not signed in.
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the sign-in screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
} else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
// The current country/region does not support IAP.
}
} else {
// Other external errors.
}
}
});
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data != null) {
// Call the parseRespCodeFromIntent method to obtain the result.
int returnCode = IapClientHelper.parseRespCodeFromIntent(data);
// Use the parseCarrierIdFromIntent method to obtain the carrier ID returned by the API.
String carrierId = IapClientHelper.parseCarrierIdFromIntent(data);
}
}
}
Showing ProductsTo show products configured to users, call the obtainProductInfo API in the app to obtain product details.
1. Construct a ProductInfoReq object, send an obtainProductInfo request, and set callback listeners OnSuccessListener and OnFailureListener to receive the request result. Pass the product ID that has been defined and taken effect to the ProductInfoReq object, and specify priceType for a product.
2. If the request is successful, a ProductInfoResult object will be returned. Using the getProductInfoList method of this object, my app can obtain the list of ProductInfo objects. The list contains details of each product, including its price, name, and description, allowing users to see the info of the products that are available for purchase.
Code:
List<String> productIdList = new ArrayList<>();
// Only those products already configured can be queried.
productIdList.add("ConsumeProduct1001");
ProductInfoReq req = new ProductInfoReq();
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setProductIds(productIdList);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the obtainProductInfo API to obtain the details of the configured product.
Task<ProductInfoResult> task = Iap.getIapClient(activity).obtainProductInfo(req);
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
// Obtain the product details returned upon a successful API call.
List<ProductInfo> productList = result.getProductInfoList();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
Initiating a PurchaseThe app can send a purchase request by calling the createPurchaseIntent API.
1. Construct a PurchaseIntentReq object to send a createPurchaseIntent request. Pass the product ID that has been defined and taken effect to the PurchaseIntentReq object. If the request is successful, the app will receive a PurchaseIntentResult object, and its getStatus method will return a Status object. The app will display the checkout screen of IAP using the startResolutionForResult method of the Status object.
Code:
// Construct a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// Only the products already configured can be purchased through the createPurchaseIntent API.
req.setProductId("CProduct1");
// priceType: 0: consumable; 1: non-consumable; 2: subscription
req.setPriceType(0);
req.setDeveloperPayload("test");
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the createPurchaseIntent API to create a product order.
Task<PurchaseIntentResult> task = Iap.getIapClient(activity).createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
// Obtain the order creation result.
Status status = result.getStatus();
if (status.hasResolution()) {
try {
// 6666 is a constant.
// Open the checkout screen returned.
status.startResolutionForResult(activity, 6666);
} catch (IntentSender.SendIntentException exp) {
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
2. After the app opens the checkout screen and the user completes the payment process (that is, successfully purchases a product or cancels the purchase), IAP will return the payment result to your app through onActivityResult. You can use the parsePurchaseResultInfoFromIntent method to obtain the PurchaseResultInfo object that contains the result information.
If the purchase is successful, obtain the purchase data InAppPurchaseData and its signature data from the PurchaseResultInfo object. Use the public key allocated by AppGallery Connect to verify the signature.
When a user purchases a consumable, if any of the following payment exceptions is returned, check whether the consumable was delivered.
Payment failure (OrderStatusCode.ORDER_STATE_FAILED).
A user has purchased the product (OrderStatusCode.ORDER_PRODUCT_OWNED).
The default code is returned (OrderStatusCode.ORDER_STATE_DEFAULT_CODE), as no specific code is available.
Code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 6666) {
if (data == null) {
Log.e("onActivityResult", "data is null");
return;
}
// Call the parsePurchaseResultInfoFromIntent method to parse the payment result.
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
switch(purchaseResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
// The user cancels the purchase.
break;
case OrderStatusCode.ORDER_STATE_FAILED:
case OrderStatusCode.ORDER_PRODUCT_OWNED:
// Check whether the delivery is successful.
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
// The payment is successful.
String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
// Verify the signature using your app's IAP public key.
// Start delivery if the verification is successful.
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
break;
default:
break;
}
}
}
Confirming a PurchaseAfter a user pays for a purchase or subscription, the app checks whether the payment is successful based on the purchaseState field in InAppPurchaseData. If purchaseState is 0 (already paid), the app will deliver the purchased product or service to the user, then send a delivery confirmation request to IAP.
For a consumable, parse purchaseToken from InAppPurchaseData in JSON format to check the delivery status of the consumable.
After the consumable is successfully delivered and its purchaseToken is obtained, your app needs to use the consumeOwnedPurchase API to consume the product and instruct the IAP server to update the delivery status of the consumable. purchaseToken is passed in the API call request. If the consumption is successful, the IAP server will reset the product status to available for purchase. Then the user can buy it again.
Code:
// Construct a ConsumeOwnedPurchaseReq object.
ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq();
String purchaseToken = "";
try {
// Obtain purchaseToken from InAppPurchaseData.
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
purchaseToken = inAppPurchaseDataBean.getPurchaseToken();
} catch (JSONException e) {
}
req.setPurchaseToken(purchaseToken);
// Obtain the Activity object.
final Activity activity = getActivity();
// Call the consumeOwnedPurchase API to consume the product after delivery if the product is a consumable.
Task<ConsumeOwnedPurchaseResult> task = Iap.getIapClient(activity).consumeOwnedPurchase(req);
task.addOnSuccessListener(new OnSuccessListener<ConsumeOwnedPurchaseResult>() {
@Override
public void onSuccess(ConsumeOwnedPurchaseResult result) {
// Obtain the execution result.
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
For a non-consumable, the IAP server returns the confirmed purchase data by default. After the purchase is successful, the user does not need to confirm the transaction, and the app delivers the product.
For a subscription, no acknowledgment is needed after a successful purchase. However, as long as the user is entitled to the subscription (that is, the value of InApppurchaseData.subIsvalid is true), the app should offer services.
ConclusionIt's a great feeling to make a game, and it's an even greater feeling when that game makes you money.
In this article, I shared my experience of building an in-app purchase function for my mobile survival game. To make it more suitable for a global market, I used some gimmicks from HMS Core In-App Purchases to configure product information in the language of the country or region where the user's account is located. In short, this streamlines the purchase journey for users wherever they are located.
Did I miss anything? I'm looking forward to hearing your ideas.
Related
In this article we will talk about how we can use Kotlin Flows with Huawei Cloud DB.
Since both Kotlin Flows and Huawei Cloud DB is really huge topic we will not cover deeply and just talk about general usage and how we can use the two together.
You can refer this article about Kotlin Flows and this documentation for Cloud DB for more and detail explanation.
Kotlin Flows
A flow is an asynchronous version of a Sequence, a type of collection whose values are lazily produced. Just like a sequence, a flow produces each value on-demand whenever the value is needed, and flows can contain an infinite number of values.
Flows are based on suspending functions and they are completely sequential, while a coroutine is an instance of computation that, like a thread, can run concurrently with the other code.
We can create a flow easily with flow builder and emit data
Code:
private fun getData() = flow {
val data = fetchDataFromNetwork()
emit(data)
}
fetchDataFromNetwork is a simple function that simulate network task
Code:
private suspend fun fetchDataFromNetwork() : Any {
delay(2000) // Delay
return Any()
}
Flows are cold which means code inside a flow builder does not run until the flow is collected.
Code:
GlobalScope.launch {
getData().collect {
LogUtils.d("emitted data: $it")
}
}
Collect flow and see emitted data.
Using flow with one-shot callback is easy but what if we have multi-shot callback? In other words, a specified callback needs to be called multiple times?
Code:
private fun getData() = flow {
myAwesomeInterface.addListener{ result ->
emit(result) // NOT ALLOWED
}
}
When we try to call emit we see an error because emit is a suspend function and suspend functions only can be called in a suspend function or a coroutine body.
At this point, Callback flow comes to rescue us. As documentation says
Creates an instance of the cold Flow with elements that are sent to a SendChannel provided to the builder’s block of code via ProducerScope. It allows elements to be produced by code that is running in a different context or concurrently.
Therefore the callback flow offers a synchronized way to do it with the offer option.
Code:
private fun getData() = callbackFlow {
myAwesomeInterface.addListener{ result ->
offer(result) // ALLOWED
}
awaitClose{ myAwesomeInterface.removeListener() }
}
The offer() still stands for the same thing. It's just a synchronized way (a non suspending way) for emit() or send()
awaitClose() is called either when a flow consumer cancels the flow collection or when a callback-based API invokes SendChannel.close manually and is typically used to cleanup the resources after the completion, e.g. unregister a callback.
Using awaitClose()is mandatory in order to prevent memory leaks when the flow collection is cancelled, otherwise the callback may keep running even when the flow collector is already completed.
Now we have a idea of how we can use flow with multi-show callback. Lets continue with other topic Huawei Cloud DB.
Huawei Cloud DB
Cloud DB is a device-cloud synergy database product that provides data synergy management capabilities between the device and cloud, unified data models, and various data management APIs.
Cloud DB enables seamless data synchronization between the device and cloud, and supports offline application operations, helping developers quickly develop device-cloud and multi-device synergy applications
After enable Cloud DB and make initializations, we can start with reading data.
First need a query for getting user data based on given accountId
Code:
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
Then we need to execute this query
Code:
val queryTask: CloudDBZoneTask<CloudDBZoneSnapshot<User>> = cloudDBZone.executeQuery(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR)
While executing a query we have to define query policy which define your priority.
POLICY_QUERY_FROM_CLOUD_PRIOR means that Cloud DB will try to fetch data from cloud if it fails it will give cached data if exist. We can also use POLICY_QUERY_FROM_LOCAL_ONLY or POLICY_QUERY_FROM_CLOUD_ONLY based on our use case.
As the last step, add success and failure callbacks for result.
Code:
queryTask
.addOnSuccessListener {
LogUtils.i("queryTask: success")
}
.addOnFailureListener {
LogUtils.e("queryTask: failed")
}
Now let’s combine these methods with callback flow
Code:
@ExperimentalCoroutinesApi
suspend fun getUserData(id : String?) : Flow<Resource<User>> = withContext(ioDispatcher) {
callbackFlow {
if (id == null) {
offer(Resource.Error(Exception("Id must not be null")))
[email protected]
}
// 1- Create query
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
// 2 - Create task
val queryTask: CloudDBZoneTask<CloudDBZoneSnapshot<User>> = cloudDBZone.executeQuery(
query,
CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR
)
try {
// 3 - Listen callbacks
offer(Resource.Loading)
queryTask
.addOnSuccessListener {
LogUtils.i("queryTask: success")
// Get user data from db
if (it.snapshotObjects != null) {
// Check item in db exist
if (it.snapshotObjects.size() == 0) {
offer(Resource.Error(Exception("User not exists in Cloud DB!")))
[email protected]
}
while (it.snapshotObjects.hasNext()) {
val user: User = it.snapshotObjects.next()
offer(Resource.Success(user))
}
}
}
.addOnFailureListener {
LogUtils.e(it.localizedMessage)
it.printStackTrace()
// Offer error
offer(Resource.Error(it))
}
} catch (e : Exception) {
LogUtils.e(e.localizedMessage)
e.printStackTrace()
// Offer error
offer(Resource.Error(e))
}
// 4 - Finally if collect is not in use or collecting any data we cancel this channel
// to prevent any leak and remove the subscription listener to the database
awaitClose {
queryTask.addOnSuccessListener(null)
queryTask.addOnFailureListener(null)
}
}
}
Resource is a basic sealed class for state management
Code:
sealed class Resource<out T> {
class Success<T>(val data: T) : Resource<T>()
class Error(val exception : Exception) : Resource<Nothing>()
object Loading : Resource<Nothing>()
object Empty : Resource<Nothing>()
}
For make it more easy and readable we use liveData builder instead of mutableLiveData.value = newValue in ViewModel
Code:
val userData = liveData(Dispatchers.IO) {
getUserData("10").collect {
emit(it)
}
}
In Activity, observe live data and get the result
Code:
viewModel.userData.observe(this, Observer {
when(it) {
is Resource.Success -> {
hideProgressDialog()
showUserInfo(it.data)
}
is Resource.Loading -> {
showProgressDialog()
}
is Resource.Error -> {
// show alert
}
is Resource.Empty -> {}
}
})
Just like one shot request above it is possible to listen live data changes with Cloud DB. In order to do that we have to subscribe snapshot.
Code:
val subscription = cloudDBZone.subscribeSnapshot(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR,
object : OnSnapshotListener<User> {
override fun onSnapshot(snapShot: CloudDBZoneSnapshot<User>?, error: AGConnectCloudDBException?) {
// do something
}
})
This callback will be called every time the data is changed.
Let’s combine with callback flow again
Code:
@ExperimentalCoroutinesApi
suspend fun getUserDataChanges(id : String?) : Flow<Resource<User>> = withContext(ioDispatcher) {
callbackFlow {
if (id == null) {
offer(Resource.Error(Exception("Id must not be null")))
[email protected]
}
// 1- Create query
val query: CloudDBZoneQuery<User> = CloudDBZoneQuery.where(User::class.java).equalTo("accountId", id)
// 2 - Register query
val subscription = cloudDBZone.subscribeSnapshot(query, CloudDBZoneQuery.CloudDBZoneQueryPolicy.POLICY_QUERY_FROM_CLOUD_PRIOR, object : OnSnapshotListener<User> {
override fun onSnapshot(snapShot: CloudDBZoneSnapshot<User>?, error: AGConnectCloudDBException?) {
// Check error
if (error != null) {
error.printStackTrace()
offer(Resource.Error(error))
return
}
// Check data
try {
val snapShotObjects = snapShot?.snapshotObjects
// Get user data from db
if (snapShotObjects != null) {
// Check item in db exist
if (snapShotObjects.size() == 0) {
offer(Resource.Error(Exception("User not exists in Cloud DB!")))
return
}
while (snapShotObjects.hasNext()) {
val user : User = snapShotObjects.next()
offer(Resource.Success(user))
}
}
} catch (e : Exception) {
e.printStackTrace()
offer(Resource.Error(e))
} finally {
snapShot?.release()
}
}
})
// 3 - Remove subscription
awaitClose {
subscription.remove()
}
}
}
From now on we can listen data changes on the cloud and show them on the ui.
Additional Notes
It should be reminded that Cloud DB is still in beta phase but works pretty well.
For upsert requests, authentication is mandatory. If authentication is not done, the result of upsert will return false. Huawei offers Account Kit and Auth Service for easy authentication
In this article we talked about how can we use Kotlin Flows with Huawei Cloud DB
Does Cloud DB supports BLOB type ?
Background
Most social networking apps offer a set of standard features, such as accurate connections with nearby people, face-to-face grouping, and short-distance local chatting in point-to-point mode. However, while the common "Nearby people" feature can help users discover others hundreds of meters, or even several kilometers away from them, it is not well suited to connect users who are in each other's presence. Likewise, for "face-to-face grouping", it would be great to connect with group members in your vicinity, and communicate with friends, even when there is no Internet connection. Thanks to Nearby Service, this long sought-after ideal is now a full-blown reality! So, without further ado, let's take a look at how you can integrate Nearby Service, to create the social networking app of your dreams.
Applicable Scenarios
Here's an example of an app that has integrated Nearby Service. With Nearby Service, the app is able to discover nearby people, and implement a myriad of different functions, such as face-to-face grouping, group chats, private chats, and chat log migration.
Nearby people
Nearby Service ensures that users can discover people within several meters, with a high degree of accuracy. The service enables users to connect with people in their vicinity, so long as they are in the same room, such as at a conference, exhibition, bar, or cafe.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Face-to-face grouping
With Nearby Service, users are able to directly create a group on their device, and set a precise location range. Only people within this location range will be eligible to join the group.
Free chat
Nearby Service facilitates short-distance group chatting, and does not require the group members to add each other. Instead, members can join the group automatically when they are within the location range, and will automatically exit the group when they leave this range.
Private chat
Nearby Service implements short-distance local chatting in point-to-point mode, enabling users to communicate with just the two devices in question, and free of any concern regarding data leakage, due to the presence of encrypted communications data that is not uploaded to the cloud. Furthermore, the feature supports communications in areas with poor signal coverage, such as on high-speed rail or the metro.
Chat log migration
Nearby Service offers a powerful file transmission function as well. The communications APIs in Nearby Service ensure that users enjoy high-speed, traffic-free data transmission when they need to migrate chat records on multiple devices.
Preparing Tools
Two or more Android phones
Android Studio (3.X or later)
Creating an App
Import the source code to Android Studio.
Register as a Huawei developer.
Create an app in Huawei AppGallery. For details, please refer to HUAWEI Developers-App Development. Note that you will need to download the agconnect-services.json file, generate the signing certificate, and save them to the app directory.
Run the adb command to install the generated APK file on the test phones.
Core Code
For more information about the APIs used in the app, please visit Nearby Service-Development Guides.
This app is created in MVP mode. For the source code, you can find the corresponding class in the code path: com\huawei\hms\nearby\im.
1.Nearby people
As shown in the following code, you will need to pass the view object during the initialization of the NearbyPeoplePresenter class. When the NearbyPeoplePresenter.findNearbyPeople() API is called to discover nearby people, call the view API to perform subsequent steps.
Code:
public NearbyPeoplePresenter(Context mContext, INearbyPeopleView view) {
super(mContext, view);
nearbyAgent = new NearbyAgent(mContext, new NearbyAgent.INearbyMessageView() {
@Override
public void onMessageFound(MessageBean messageBean) {
// notify view when found someone
view.onMemberChanged(false,messageBean);
}
@Override
public void onMessageLost(MessageBean messageBean) {
view.onMemberChanged(true,messageBean);
}
@Override
public void onMsgSendResult(boolean isSucceed, MessageBean item) {
view.onLoginResult(isSucceed,item);
if (!isSucceed) {
handler.postDelayed(() -> findNearbyPeople(),DURATION_RE_LOGIN);
}
}
});
handler = new Handler(Looper.getMainLooper());
}
public void findNearbyPeople() {
nearbyAgent.broadcastMessage(null,MessageBean.ACTION_TAG_ONLINE);
nearbyAgent.startScan();
}
2.Face-to-face grouping
This process is similar to that for nearby people. Pass the view object during the initialization of the CreateGroupPresenter.java class. When the joinGroup(groupId) API is called, the user is added to the group. The result is returned by calling the view API.
INearbyPeopleView and xxxView in the following steps are a group of APIs. Their implementation is the corresponding Activity, which you can view through the code path: com\huawei\hms\nearby\im\ui\adapter.
Code:
public CreateGroupPresenter(Context mContext, ICreateGroupView view) {
super(mContext, view);
nearbyAgent = new NearbyAgent(mContext, new NearbyAgent.INearbyMessageView() {
@Override
public void onMessageFound(MessageBean messageBean) {
view.onPeopleFound(messageBean);
}
@Override
public void onMessageLost(MessageBean messageBean) {}
@Override
public void onMsgSendResult(boolean isSucceed, MessageBean item) {
view.onJoinGroupResult(isSucceed,item);
}
});
}
public void joinGroup(String groupId) {
nearbyAgent.broadcastMessage(groupId,"join group");
nearbyAgent.startScan(groupId);
}
3.Free chat
Pass the view object during the initialization of the GroupChatPresenter.java class. You can call broadcastMessage(groupId, sendContent) to send a message to a specified group. The group is not limited if groupId is null. Also, the findMessage(groupId) API can be called to find the message of a specified group. After the message is found, the view API will be called to return the message.
Code:
public GroupChatPresenter(Context mContext, IGroupChatView view) {
super(mContext, view);
nearbyAgent = new NearbyAgent(mContext, new NearbyAgent.INearbyMessageView() {
@Override
public void onMessageFound(MessageBean messageBean) {
view.onMessageFound(messageBean);
}
@Override
public void onMessageLost(MessageBean messageBean) {
}
@Override
public void onMsgSendResult(boolean isSucceed, MessageBean item) {
view.onMsgSendResult(isSucceed,item);
}
});
}
public void broadcastMessage(String groupId, String sendContent) {
nearbyAgent.broadcastMessage(groupId,sendContent);
}
public void findMessage(String groupId) {
nearbyAgent.startScan(groupId);
}
4.Private chat
Private chat implementation differs from the process above. The NearbyConnectionPresenter.java class provides the following APIs:
findNearbyPeople(): Discovering people nearby.
requestConnect(): Connecting users with each other.
sendMessage(String msgStr): Sending messages of strings.
sendFile(Uri uri): Sending files.
Code:
/**
* scanAndBroadcasting to find nearby people
*/
public void findNearbyPeople(){
mDiscoveryEngine.startScan(serviceId, new ScanEndpointCallback() {
@Override
public void onFound(String endpointId, ScanEndpointInfo discoveryEndpointInfo) {
Log.d(TAG, "onFound -- Nearby Connection Demo app: onFound endpoint: " + endpointId);
view.onFound(endpointId,discoveryEndpointInfo);
}
@Override
public void onLost(String endpointId) {
Log.d(TAG, "onLost -- Nearby Connection Demo app: Lost endpoint: " + endpointId);
view.onLost(endpointId);
}
}, scanOption);
}
/**
* request to connect with remote device
* @param endpointId the endpointId of remote device
*/
public void requestConnect(String endpointId) {
Log.d(TAG, "requestConnect -- endpoint: " + endpointId);
mDiscoveryEngine.requestConnect(myNameStr, endpointId, connectCallback);
}
/**
* Send message ,Data.Type.BYTES
*/
public MessageBean sendMessage(String msgStr) {
MessageBean item = new MessageBean();
item.setUserName(CommonUtil.userName);
item.setMsg(msgStr);
item.setType(MessageBean.TYPE_SEND_TEXT);
item.setSendTime(DateUtils.getCurrentTime(DateUtils.FORMAT));
Data data = Data.fromBytes(gson.toJson(item).getBytes(Charset.defaultCharset()));
mTransferEngine.sendData(mEndpointId, data);
return item;
}
/**
* send file ,Data.Type.FILE
* @param uri
*/
public Data sendFile(Uri uri) {
Data filePayload;
try {
ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(uri, "r");
filePayload = Data.fromFile(pfd);
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "File not found, cause: ", e);
return null;
}
String fileName = FileUtil.getFileRealNameFromUri(mContext, uri);
String filenameMessage = filePayload.getId() + ":" + fileName;
Data filenameBytesPayload = Data.fromBytes(filenameMessage.getBytes(StandardCharsets.UTF_8));
mTransferEngine.sendData(mEndpointId, filenameBytesPayload);
mTransferEngine.sendData(mEndpointId, filePayload);
return filePayload;
}
Learn More
For more detailed information, please visit HUAWEI Developers.
For further instructions, please visit Development Guides.
You can join the HMS Core developer discussion by going to Reddit.
You can download the demo and sample code on GitHub.
To resolve any integration issues, please go to Stack Overflow.
Subscriptions are mostly used for video, music, and reading apps. Once a user has purchased a subscription, it can be automatically renewed on a periodic basis.
The graph below shows the process for purchasing a subscription.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
This guide tells you how to integrate HUAWEI In-App Purchases' (IAP) product management function so you can manage subscriptions in your apps. It will also provide solutions for problems that can occur during the integration.
Key development steps:
1. Create a subscription in the product management system (PMS) and add it to your app. You can do this by referring to Adding a Product.
Notes:
(1) Before you create a subscription, you need to create a subscription group to manage subscriptions of the same type.
(2) A new subscription in PMS will be automatically allocated to a default group, so you cannot choose a specific group for it. The subscription also cannot be deleted or re-created. Fixing such problems takes a great deal of effort, especially when the product, whose ID stays the same, is released on multiple platforms.
(3) When you add a subscription, its status will be set to Invalid by default. You need to activate it before you can start selling it. You can present invalid subscriptions in your app, but users will be unable to purchase them.
2. How the Code Works
a. Check whether the country or region of the signed-in HUAWEI ID is supported by HUAWEI IAP.
Code:
/**
* Check whether the country or region of the signed-in HUAWEI ID is included in the countries or regions
* supported by HUAWEI IAP.
* @param activity indicates the activity object that initiates a request.
*/
public static void isBillingSupported(final Activity activity) {
// Get the Activity instance that calls this API.
Task<IsEnvReadyResult> task = Iap.getIapClient(activity).isEnvReady();
task.addOnSuccessListener(new OnSuccessListener<IsEnvReadyResult>() {
@Override
public void onSuccess(IsEnvReadyResult result) {
// Obtain the execution result.
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
if (status.getStatusCode() == OrderStatusCode.ORDER_HWID_NOT_LOGIN) {
// Not signed in.
if (status.hasResolution()) {
try {
// 3333 is an int constant defined by the developer.
status.startResolutionForResult(activity, 3333);
} catch (IntentSender.SendIntentException exp) {
}
}
} else if (status.getStatusCode() == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED) {
// The current country or region is not supported by HUAWEI IAP.
}
}
}
});
}
Notes:
(1) If a user tries to purchase a subscription in your app without first signing in to their HUAWEI ID, an error code will be reported to this API. Set the API to display the sign-in page upon receiving this code.
(2) If a user tries to purchase a subscription of your app from a location where HUAWEI IAP payment is not supported, an error code will be reported to this API.
(3) It is recommended that the context of the main activity be used, because this allows you to directly display the sign-in page and process subsequent services after sign-in.
It is best to call this API before the subscription list is displayed to users.
b. Obtain in-app product details configured in AppGallery Connect.
Code:
//**
* Obtain in-app product details configured in AppGallery Connect.
* @param productIds ID list of products to be queried.
* Each product ID must exist and be unique in the current app.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
private void queryProducts(final Activity activity) {
// Pass in the productId list of products to be queried.
List<String> productIdList = new ArrayList<>();
// The product ID is the same as that set by a developer when configuring product information
// in AppGallery Connect.
productIdList.add("SubProduct1001");
ProductInfoReq req = new ProductInfoReq();
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
req.setPriceType(IapClient.PriceType.IN_APP_SUBSCRIPTION);
req.setProductIds(productIdList);
// Call the obtainProductInfo API.
Task<ProductInfoResult> task = Iap.getIapClient(activity).obtainProductInfo(req);
task.addOnSuccessListener(new OnSuccessListener<ProductInfoResult>() {
@Override
public void onSuccess(ProductInfoResult result) {
// Obtain the result.
List<ProductInfo> productList = result.getProductInfoList();
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
}
When calling this API, errors can occur if:
(1) The product type parameter does not match the product. For example, the value 0 (consumable) is passed to the parameter type for a subscription.
(2) More than 200 products are imported in one batch.
(3) The product ID is different from the one configured in the PMS.
(4) The product has not been activated.
c. Create orders for in-app products in the PMS.
Code:
/**
* Create orders for in-app products in the PMS.
*
* @param productId ID of the in-app product to be paid.
* The in-app product ID is the product ID you set during in-app product
* configuration in AppGallery Connect.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
private void buy(final Activity activity, String productId, int type) {
// Construct a PurchaseIntentReq object.
PurchaseIntentReq req = new PurchaseIntentReq();
// The product ID is the same as that set by a developer when configuring product information in AppGallery Connect.
req.setProductId("productId");
// In-app product type contains:
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
// type:IapClient.PriceType.IN_APP_SUBSCRIPTION
req.setPriceType(type);
req.setDeveloperPayload("developer define info");
// Call the createPurchaseIntent API.
Task<PurchaseIntentResult> task = Iap.getIapClient(activity).createPurchaseIntent(req);
task.addOnSuccessListener(new OnSuccessListener<PurchaseIntentResult>() {
@Override
public void onSuccess(PurchaseIntentResult result) {
// Obtain the payment result.
Status status = result.getStatus();
if (status.hasResolution()) {
try {
// 6666 is an int constant defined by the developer.
status.startResolutionForResult(activity,status.startResolutionForResult(activity, Constants.REQ_CODE_BUY));
} catch (IntentSender.SendIntentException exp) {
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
}
When calling this API, errors can occur if:
(1) The product is still invalid after it has been purchased.
(2) The payment page does not display. This can happen if the parameter or context is incorrect. We recommend you use the context of the main activity, because this will enable your app to provide services after the user has made a successful payment.
(3) The user has to sign the payment agreement because they are purchasing a subscription for the first time.
d. HUAWEI IAP returns the payment result to your app through the main activity's onActivityResult callback.
Code:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == Constants.REQ_CODE_LOGIN || requestCode == Constants.REQ_CODE_BUYWITHPRICE_CONTINUE) {
int returnCode = IapClientHelper.parseRespCodeFromIntent(data);
Log.i(TAG,"onActivityResult, returnCode: " + returnCode);
if (returnCode == OrderStatusCode.ORDER_STATE_SUCCESS) {
// If success is returned, you can call the buyWithPrice API again.
//buyWithPrice(productId);
} else if(returnCode == OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED){
Log.e(TAG,"This is unavailable in your country/region." );
} else {
Log.e(TAG,"user cancel login" );
}
return;
}
if (requestCode == Constants.REQ_CODE_BUY) {
if (data == null) {
Log.e("onActivityResult", "data is null");
return;
}
PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data);
switch(purchaseResultInfo.getReturnCode()) {
case OrderStatusCode.ORDER_STATE_CANCEL:
// The user cancels payment.
break;
case OrderStatusCode.ORDER_STATE_FAILED:
case OrderStatusCode.ORDER_PRODUCT_OWNED:
// Check if there exists undelivered products.
break;
case OrderStatusCode.ORDER_STATE_SUCCESS:
// The payment is successful.
String inAppPurchaseData = purchaseResultInfo.getInAppPurchaseData();
String inAppPurchaseDataSignature = purchaseResultInfo.getInAppDataSignature();
// Use the public key of your app to verify the signature.
// If the signature is correct, you can deliver your products.
// If the user purchased a consumable product, call the consumeOwnedPurchase
// API to consume it after successfully delivering the product.
break;
default:
break;
}
}
}
Notes:
(1) This API is used for payment results like payment cancellations, abnormal purchasing activity, payment successes, or when the user tries to purchase a product they have already purchased.
(2) If a user tries to purchase a subscription without first signing in to their HUAWEI ID, set this API to resume processing after they have signed in.
(3) If a user tries to purchase a subscription without having first signed the payment agreement, set this API to enable your app to continue the purchasing process after the user has signed the agreement.
e. Once a user has successfully purchased a subscription, verify the purchase on your app to check that the subscription is valid and provides its service.
Code:
/**
* Deliver products for the user .
*/
private void deliverProduct(final String inAppPurchaseDataStr, final String inAppPurchaseDataSignature) {
if (TextUtils.isEmpty(inAppPurchaseDataStr) || TextUtils.isEmpty(inAppPurchaseDataSignature)) {
Utils.showMessage(this, "purchase data is error");
return;
}
if (CipherUtil.doCheck(inAppPurchaseDataStr, inAppPurchaseDataSignature, Key.getPublicKey())) {
try {
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseDataStr);
if(inAppPurchaseDataBean.isSubValid())
{
// Provide the user with the service.
}
} catch (JSONException e) {
Log.e(TAG, "delivery:" + e.getMessage());
}
} else {
Log.e(TAG, "delivery:" + getString(R.string.verify_signature_fail));
}
}
Notes:
(1) You need to generate the public key used for verifying signatures by going to Project Setting > In-App Purchases > Settings in AppGallery Connect.
(2) After a successful verification, for services (such as video app memberships) that are only available when they have been renewed in time, if InApppurchaseData.subIsvalid is true, you need to keep providing the services for the user.
(3) If possible, store the delivered products' purchaseToken on your app server. This can then be used as a credential when a user switches between subscriptions or accounts.
f. Redelivery. This mechanism reduces unsuccessful deliveries.
Generally, if an exception (such as a network error or process failure) occurs after the subscription has been paid for, your app will be unable to detect whether the payment was successful. This means the purchased product may not be delivered as expected.
However, with HUAWEI IAP, you can redeliver products.
Code:
/**
* Query information about all subscribed in-app products, including consumables, non-consumables,
* and auto-renewable subscriptions.
* If consumables are returned, the system needs to deliver them and calls the consumeOwnedPurchase
* API to consume the products.
* If non-consumables are returned, the in-app products do not need to be consumed.
* If subscriptions are returned, all existing subscription relationships of the user under the
* app are returned.
* @param mClient IapClient instance to call the obtainOwnedPurchases API.
* @param type In-app product type.
* The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription
*/
public static void obtainOwnedPurchases(IapClient mClient, final int type) {
Log.i(TAG, "call obtainOwnedPurchases");
String continuationToken = null;
// Construct a OwnedPurchasesReq object.
OwnedPurchasesReq ownedPurchasesReq = new OwnedPurchasesReq();
// In-app product type contains:
// priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
ownedPurchasesReq.setPriceType(2);
// Call the obtainOwnedPurchases API.
// Get the Activity instance that calls this API.
Task<OwnedPurchasesResult> task = mClient.obtainOwnedPurchases(ownedPurchasesReq);
task.addOnSuccessListener(new OnSuccessListener<OwnedPurchasesResult>() {
@Override
public void onSuccess(OwnedPurchasesResult result) {
// Obtain the execution result.
if (result != null && result.getInAppPurchaseDataList() != null) {
for (int i = 0; i < result.getInAppPurchaseDataList().size(); i++) {
String inAppPurchaseData = result.getInAppPurchaseDataList().get(i);
String InAppSignature = result.getInAppSignature().get(i);
// Use the payment public key to verify the signature of the inAppPurchaseData.
deliverProduct(inAppPurchaseData,InAppSignature);
// The verification is successful.
try {
InAppPurchaseData inAppPurchaseDataBean = new InAppPurchaseData(inAppPurchaseData);
int purchaseState = inAppPurchaseDataBean.getPurchaseState();
} catch (JSONException e) {
}
}
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
if (e instanceof IapApiException) {
IapApiException apiException = (IapApiException) e;
Status status = apiException.getStatus();
int returnCode = apiException.getStatusCode();
} else {
// Other external errors.
}
}
});
}
Notes:
(1) This mechanism works by either querying the IAP server for information about all valid, in-app product subscriptions, and then reporting this to your app server and providing services, or by providing services directly from the client.
(2) For services (such as video app memberships) that are only available when they have been renewed in time, if InApppurchaseData.subIsvalid is true, you need to keep providing the services for the user. For other custom services (such as magazine subscriptions), if you require the subscription renewal data for previous periods, you can query the receipts for all subscriptions through IapClient.obtainOwnedPurchaseRecord. The receipt format is the same as it is in InAppPurchaseData.
(3) If InApppurchaseData.purchaseState is set to 0, the subscription is in purchased state. The time segment from InApppurchaseData.purchaseTime to InApppurchaseData.expirationDate indicates a subscription's validity period.
(4) If a subscription is in purchased state and its validity period matches your app server's valid provisioning period, your app needs to provide the relevant services. For all subscriptions, your app determines whether to provide services in this way.
3. Key subscription event callback from the Huawei IAP Server
(1) Before you request callback, you need to sign in to AppGallery Connect and go to Project Setting > In-App Purchases > Settings to generate a public key and app server callback address.
(2) Phases of the subscription process:
Promotional period: The user enjoys a subscription for free or at a promotional price. This period will begin when the user successfully subscribes.
Renewal:
First renewal: The system renews the subscription at the standard price. During this period, the subscription is valid and the user is entitled to the service provided by the subscription.
Second and subsequent renewals: For each of these renewals, HUAWEI IAP renews the subscription by periodically deducting fees from the user's account. It obtains the fee and product status 10 days before the subscription expires. HUAWEI IAP will attempt to deduct fees from the user's account 24 hours before the subscription renews. If this fails, the lapse period will start. During this period, HUAWEI IAP will try to deduct fees once a day. It will stop trying when this period ends.
Expiring: The subscription is valid and services are available. However, because the user has canceled the subscription, no fee will be deducted for the next renewal.
Expired: The subscription has expired and services are no longer available. There are several possible causes for this: HUAWEI IAP was unable to deduct a fee for the last renewal; the user has canceled the subscription; the user has not agreed to a price increase; the fee deducted account is abnormal.
To-be-effective: The subscription has not taken effect, and the user cannot use the service provided by the subscription. This period occurs when a user switches between subscriptions in the same subscription group. For example, a user may have switched to a yearly subscription from a monthly one which is still valid, in which case the yearly subscription would be in the to-be-effective period.
Suspended: Users can decide on a suspension plan for a subscription which is under renewal on the subscription management screen of HMS Core (APK). Once this plan is completed, the subscription renewal will be suspended, but the suspension will not take effect and users can still use the services provided by the subscription. Once the subscription validity period has ended, the subscription renewal will be suspended, and the user will no longer be able to use the services. Then, after the suspension period ends, the subscription will be automatically renewed. If the renewal is successful, the subscription can be used. If the renewal fails, the subscription will expire. Users can adjust suspension plans at any time, and the maximum suspension period is three months. Users can cancel the suspension plan at any time to revert to the auto-renewal status and enjoy the services immediately.
Grace period (coming soon): If you enable a grace period for your app, it will activate when HUAWEI IAP is unable to deduct a fee at the end of the subscription validity period. During the grace period, the user can still use the subscription while the HUAWEI IAP server attempts to deduct fees. If the fee deduction is successful within the grace period, the renewal will go ahead as planned. If the fee deduction is successful once the grace period has passed, the renewal will be interrupted. Then, the renewal start time will be the actual fee deduction time.
Lapse period: If a subscription is not renewed (for example, the user proactively cancels a renewal) or the renewal fails after the grace period has passed, a subscription enters the lapse period. For a subscription in the lapse period, there is a 30-day retention period when services are unavailable but a user can still resume the subscription. During this period, the subscription can be considered as having expired.
Key subscription event callback returned to your app server:
POST /subscription/notify/address
Content-Type: application/json; charset=UTF-8
Accept: application/json
Content-Length: 2754
Code:
{
"statusUpdateNotification": "{\"environment\":\"Sandbox\",\"notificationType\":7,\"subscriptionId\":\"1581789719266.D40972AC.3089\",\"orderId\":\"1581789719266.148748E7.3089\",\"latestReceipt\":\"00000173741056a37eef310dff9c6a86fec57efafe318ae478e52d9c4261994d64c8f6fc8ea1abbdx5347.5.3089\",\"latestReceiptInfo\":\"{\\\"autoRenewing\\\":true,\\\"subIsvalid\\\":true,\\\"orderId\\\":\\\"1581789719266.148748E7.3089\\\",\\\"lastOrderId\\\":\\\"1581790584090.DDB1EE93.5610\\\",\\\"packageName\\\":\\\"com.example.hwinappdemo\\\",\\\"applicationId\\\":123456,\\\"productId\\\":\\\"monthly_subscription2\\\",\\\"kind\\\":2,\\\"productName\\\":\\\"monthly_subscription2\\\",\\\"productGroup\\\":\\\"185F2E6A04BB4299B82C2525D3D68EBE\\\",\\\"purchaseTime\\\":1582791622434,\\\"oriPurchaseTime\\\":1582790122434,\\\"purchaseState\\\":0,\\\"developerPayload\\\":\\\"test\\\",\\\"purchaseToken\\\":\\\"00230173741056a37eef310dff9c6a86fec57efafe318ae478e52d9c4261994d64c8f6fc8ea1abbdx5347.5.3089\\\",\\\"purchaseType\\\":0,\\\"currency\\\":\\\"TRY\\\",\\\"price\\\":2,\\\"country\\\":\\\"TR\\\",\\\"subscriptionId\\\":\\\"1581789719266.D40972AC.3089\\\",\\\"quantity\\\":1,\\\"daysLasted\\\":26,\\\"numOfPeriods\\\":6,\\\"numOfDiscount\\\":0,\\\"expirationDate\\\":1582791922434,\\\"retryFlag\\\":1,\\\"introductoryFlag\\\":0,\\\"trialFlag\\\":0,\\\"renewStatus\\\":1,\\\"cancelledSubKeepDays\\\":30,\\\"payOrderId\\\":\\\"SandBox_1581789719266.148748E7.3089\\\",\\\"payType\\\":\\\"0\\\",\\\"acknowledged\\\":1,\\\"cancelWay\\\":0,\\\"cancellationTime\\\":1582789943392}\",\"latestReceiptInfoSignature\":\"S94effXhWisNXNE+lFBPv5DHGixYDY+3aI8fNDA+/wGQA+gZEx76Vilf0OqmW7zWorenMp98ra98nO7zJwmoZIF7R8/ZEd0V7dWiMOU+iMkhfDpfjAmtkraj5tjTYJR+QIhrQQWIxXg1AhqmGBuTyBdy0TVrfsVoKfCnh5oFZ5H7UB7jZA3KJtIHqsziAFdPEP67oE9kf4eHNsYAw/xSl4wTPXgsRJINu6SmCWcUzZ2UNMN/zWVgiqYlc9CGu6jParOfYeEnUCspdyZrsdWkmh2IB3pXOV8JxVsinzkklPwjotU21Oja46mjW9E+WSEomSjk9dop/iA0SjbzuH8pSogu0UyeO9FelFf/ZsaVQZLLjCHBK6pQ3SuHD3amqRB0GP3lGKdD1z5N7N5rO8ngS+mmlPi1gZwj+FiXdScVbmX6fOpvP+PFhyOginZp84EenKYx9Z9asXH+cJxmw9usN95cL7gAFhk+pu19Ng0Ro+t0+DPuj+cTOzyhVy7FTcDM\",\"autoRenewStatus\":1,\"productId\":\"monthly_subscription2\",\"applicationId\":\"123456\"}",
"notifycationSignature": "lD8NiUCrH+TK7gWBJB6XP3ZG9L2/TZvAGPIvRetAus9ZrKLjIzeagGjfi9rW9f/wEctIrp2gtDZ+k9xEyFT9apAqEdp629pUmufKe8jWlj6ZrHxqABcBQ2+ZFcDQH2u1Vm8tsEsk0ApQ4Z/nY2GCpifU/L6ITqT/Gn5hEBm7aCBEzOzg+SOBlCxqMOehusgPQTss32Y+E7KAPFy3SzH7o4/mMWOBoqvhZQrs/t9hlS8oamnsm3MmT3fNkoV8W/H7ckZhA62bfryjO8B/RVmz69x4CbvuIXj5Uo459aaIX6wE/5gPEqnnj1QrzYd10arNk6u0/nPUoKllm1NJ56qH/i9SLX+fRVnRnhvM/6t01IXcfxOg5Nx4pT2R/UkOEdcZs7lRrnWnVY7VsMQVZAkxbPAe1eSyzhGnW7s0mr1VfdMmr56eQ3zxNeQ2KXzeVQkBCb4EiY0MB2swFMzsWlo/OMBrUjG7ojflq0Shww45bSOBNO0nlz9B/keFj17fUKFt"
}
Case study:
In this example, a social media app integrates HUAWEI IAP. Below, you can see the kinds of major problems which may arise during the integration process, when they might occur, and how they can be solved.
1. Is there a redelivery mechanism available for subscriptions?
Scenario: The redelivery mechanism should take effect after a successful payment, but sometimes the purchased product is not delivered as expected because an exception occurs. For example, your app may fail to receive or correctly handle notifications about key subscription events, or the process may be interrupted by an unexpected app exit caused by a program error.
Solution: When your app restarts, set it to call obtainOwnedPurchases(OwnedPurchasesReq ownedPurchasesReq) to query information about all the auto-renewal subscriptions the user has purchased, and return the results. This information can be obtained from InAppPurchaseData and returned to your app server. It ensures your app can provide users with uninterrupted services. The redelivery mechanism can be implemented for all subscriptions using this kind of API.
2. When does the parameter subscriptionId change? Will it change when a user switches between subscriptions, or when a user resumes a subscription?
Scenario: This parameter is used to provide service credentials to users whom you want to be able to enjoy your service.
Solution: The subscription ID of a product remains the same when the subscription renewal is normal.
Currently, the subscription ID only changes in the following two scenarios:
l A user purchases a new product.
l A user resumes a subscription after the grace period. If this happens, the subscription ID changes because the user's previous subscription has become invalid. However, if a user proactively resumes a subscription before it enters the lapse period, the subscription ID will not change.
This is also true of the subscription's purchaseToken.
3. What notifications about key subscription events indicate the subscription is valid?
Scenario: Your app needs to be able to deliver a product to the user based on the notification types it receives.
Solution:
(1) For details about the notification types, refer to the Table above.
(2) The app will provide users with the services they are entitled once it has checked that InApppurchaseData.subIsvalid is set to true and the period from InApppurchaseData.purchaseTime to InApppurchaseData.expirationDate indicates that the subscription is valid.
4. What should I do with the subscriptions when my app supports multiple accounts for the same user?
Scenario 1: Your app has its own account system. For example, a user has two accounts, A and B. If this user purchases a monthly subscription for A, will this subscription be available for B?
Solution: HUAWEI IAP only distinguishes between HUAWEI IDs, and not user accounts registered to your app. Therefore, the purchased subscription is shared by all accounts as long as they are related to the same HUAWEI ID. In this scenario, we recommend that the user still be entitled to the service when signing in with B, up until the subscription they purchased for A has expired. Since the subscription ID stays the same, you can build an agreement mechanism according to your needs.
Scenario 2: A user purchases a monthly subscription for account A and then a yearly subscription for account B. Which of these subscriptions should my app provide them with?
Solution: In this scenario, if the user switches between subscriptions using HUAWEI IAP's subscription management feature, you can use setDeveloperPayLoad("xxxx") to set user tags and provide account B with the service when the yearly subscription becomes valid. However, when the user switches between subscriptions using the subscription switching function, your app cannot tell which account switches the subscription. Therefore, your app will provide the yearly subscription to account A, which the user used first to purchase a subscription in your app.
Scenario 3: What happens when a user disables switching between accounts A and B, cancels a subscription of A, and resumes the subscription for B?
Solution: The subscription's service will be provided to whichever user account is bound to the subscription ID. In this scenario, since this ID remains unchanged, the service could be provided to either A or B. To avoid this issue, your app server needs to unbind the subscription from A and provide the service to B.
Very interesting and helpful
Hmm
Very useful post.
View attachment Featured image.jpg
As the start of the new academic year approaches, many college students will be leaving their parents to start college life. However, a lack of experience makes it easy for college students to become victims of electronic fraud such as phone scams.
The start of the new academic year is often a period of time that sees an uptick in phone scams, especially those targeting college students. Some scammers trick students to download and register an account on malicious financial apps embedded with viruses and Trojan horses or ones that imitate legitimate apps. With such malicious apps installed on students' phones, scammers are able to steal students' sensitive data, such as bank card numbers and passwords. Some scammers trick students, by offering them small gifts or coupons, to scan QR codes which then direct them to pages that ask users to enter their personal information, such as their phone number and address. Once a student has done this, they will receive a large number of fraudulent calls and junk SMS messages from then on. If the students scan QR codes linking to phishing websites, their personal data may be leaked and sold for malicious purposes. Some scammers even lie about offering students scholarships or grants, in order to trick them into visiting phishing websites and entering their bank account numbers and passwords, causing significant financial losses to such students.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
To deal with the ever-changing tricks of fraudsters, an app needs to detect phishing websites, malicious apps, and other risks and remind users to be on the lookout for such risks with in-app tips, in order to keep users and their data safe. So, is there a one-stop service that can enhance app security from multiple dimensions? Fortunately, HMS Core Safety Detect can help developers quickly build security capabilities into their apps, and help vulnerable user groups such as college students safeguard their information and property.
The AppsCheck API in Safety Detect allows your app to obtain a list of malicious apps installed on a user's device. The API can identify 99% of malicious apps and detect unknown threats based on app behavior. Your app can then use this information to determine whether to restrict users from performing in-app payments and other sensitive operations.
The URLCheck API in Safety Detect checks whether an in-app URL is malicious. If the URL is determined to be malicious, the app can warn the user of the risk or block the URL.
Safety Detect also provides capabilities to check system integrity and detect fake users, helping developers quickly improve their app security. The integration process is straightforward, which I'll describe below.
Demo
Integration ProcedurePreparationsYou can follow the instructions here to prepare for the integration.
Using the AppsCheck APIYou can directly call getMaliciousAppsList of SafetyDetectClient to obtain a list of malicious apps. The sample code is as follows:
Code:
private void invokeGetMaliciousApps() {
SafetyDetectClient appsCheckClient = SafetyDetect.getClient(MainActivity.this);
Task task = appsCheckClient.getMaliciousAppsList();
task.addOnSuccessListener(new OnSuccessListener<MaliciousAppsListResp>() {
@Override
public void onSuccess(MaliciousAppsListResp maliciousAppsListResp) {
// Indicates that communication with the service was successful.
// Use resp.getMaliciousApps() to obtain a list of malicious apps.
List<MaliciousAppsData> appsDataList = maliciousAppsListResp.getMaliciousAppsList();
// Indicates that the list of malicious apps was successfully obtained.
if(maliciousAppsListResp.getRtnCode() == CommonCode.OK) {
if (appsDataList.isEmpty()) {
// Indicates that no known malicious apps were detected.
Log.i(TAG, "There are no known potentially malicious apps installed.");
} else {
Log.i(TAG, "Potentially malicious apps are installed!");
for (MaliciousAppsData maliciousApp : appsDataList) {
Log.i(TAG, "Information about a malicious app:");
// Use getApkPackageName() to obtain the APK name of the malicious app.
Log.i(TAG, "APK: " + maliciousApp.getApkPackageName());
// Use getApkSha256() to obtain the APK SHA-256 of the malicious app.
Log.i(TAG, "SHA-256: " + maliciousApp.getApkSha256());
// Use getApkCategory() to obtain the category of the malicious app.
// Categories are defined in AppsCheckConstants.
Log.i(TAG, "Category: " + maliciousApp.getApkCategory());
}
}
}else{
Log.e(TAG,"getMaliciousAppsList failed: "+maliciousAppsListResp.getErrorReason());
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
// An error occurred during communication with the service.
if (e instanceof ApiException) {
// An error with the HMS API contains some
// additional details.
ApiException apiException = (ApiException) e;
// You can retrieve the status code using the apiException.getStatusCode() method.
Log.e(TAG, "Error: " + SafetyDetectStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " + apiException.getStatusMessage());
} else {
// A different, unknown type of error occurred.
Log.e(TAG, "ERROR: " + e.getMessage());
}
}
});
}
Using the URLCheck API1. Initialize the URLCheck API.
Before using the URLCheck API, you must call the initUrlCheck method to initialize the API. The sample code is as follows:
Code:
SafetyDetectClient client = SafetyDetect.getClient(getActivity());
client.initUrlCheck();
2. Request a URL check.
You can pass target threat types to the URLCheck API as parameters. The constants in the UrlCheckThreat class include the current supported threat types.
Code:
public class UrlCheckThreat {
// URLs of this type are marked as URLs of pages containing potentially malicious apps (such as home page tampering URLs, Trojan-infected URLs, and malicious app download URLs).
public static final int MALWARE = 1;
// URLs of this type are marked as phishing and spoofing URLs.
public static final int PHISHING = 3;
}
a. Initiate a URL check request.
The URL to be checked contains the protocol, host, and path but does not contain the query parameter. The sample code is as follows:
Code:
String url = "https://developer.huawei.com/consumer/cn/";
SafetyDetect.getClient(this).urlCheck(url, appId, UrlCheckThreat.MALWARE, UrlCheckThreat.PHISHING).addOnSuccessListener(this, new OnSuccessListener<UrlCheckResponse >(){
@Override
public void onSuccess(UrlCheckResponse urlResponse) {
if (urlResponse.getUrlCheckResponse().isEmpty()) {
// No threat exists.
} else {
// Threats exist.
}
}
}).addOnFailureListener(this, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// An error occurred during communication with the service.
if (e instanceof ApiException) {
// HMS Core (APK) error code and corresponding error description.
ApiException apiException = (ApiException) e;
Log.d(TAG, "Error: " + CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()));
// Note: If the status code is SafetyDetectStatusCode.CHECK_WITHOUT_INIT,
// you did not call the initUrlCheck() method or you have initiated a URL check request before the call is completed.
// If an internal error occurs during the initialization, you need to call the initUrlCheck() method again to initialize the API.
} else {
// An unknown exception occurred.
Log.d(TAG, "Error: " + e.getMessage());
}
}
});
b. Call the getUrlCheckResponse method of the returned UrlCheckResponse object to obtain the URL check result.
The result contains List<UrlCheckThreat>, which includes the detected URL threat type. If the list is empty, no threat is detected. Otherwise, you can call getUrlCheckResult in UrlCheckThreat to obtain the specific threat code. The sample code is as follows:
Code:
final EditText testRes = getActivity().findViewById(R.id.fg_call_urlResult);
List<UrlCheckThreat> list = urlCheckResponse.getUrlCheckResponse();
if (list.isEmpty()) {
testRes.setText("ok");
}
else{
for (UrlCheckThreat threat : list) {
int type = threat.getUrlCheckResult();
}
}
3. Close the URL check session.
If your app does not need to call the URLCheck API anymore or will not need to for a while, you can call the shutdownUrlCheck method to close the URL check session and release relevant resources.
Code:
SafetyDetect.getClient(this).shutdownUrlCheck();
ConclusionElectronic fraud such as phone scams are constantly evolving and becoming more and more difficult to prevent, bringing great challenges to both developers and users. To combat such risks, developers must utilize technical means to identify phishing websites, malicious apps, and other risks, in order to safeguard users' personal information and property.
In this article, I demonstrated how HMS Core Safety Detect can be used to effectively combat electronic fraud. The whole integration process is straightforward and cost-efficient, and is a quick and effective way to build comprehensive security capabilities into an app.
Mobile apps have significantly changed the way we live, bringing about greater convenience. With our mobiles we can easily book hotels online when we go sightseeing, buy train and flight tickets online for business trips, or just pay for a dinner using scan and pay.
There is rarely a one-app-fits-all approach of offering such services, so users have to switch back and forth between multiple apps. This also requires users to register and sign in to different apps, which is a trouble itself because users will need to complete complex registration process and repeatedly enter their account names and passwords.
In addition, as technology develops, a developer usually has multiple Android apps and app versions, such as the quick app and web app, for different platforms. If users have to repeatedly sign in to different apps or versions by the same developer, the churn rate will likely increase. What's more, the developer may need to even pay for sending SMS messages if users choose to sign in to their apps through SMS verification codes.
Is there anything the developer can do to streamline the sign-in process between different apps and platforms so that users do not need to enter their account names and passwords again and again?
Well fortunately, HMS Core Keyring makes this possible. Keyring is a Huawei service that offers credential management APIs for storing user credentials locally on users' Android phones and tablets and sharing the credentials between different apps and different platform versions of an app. Developers can call relevant APIs in their Android apps, web apps, or quick apps to use Keyring services, such as encrypt the sign-in credentials of users for local storage on user devices and share the credentials between different apps and platforms, thus creating a seamless sign-in experience for users across different apps and platforms. Besides, all credentials will be stored in Keyring regardless of which type of APIs developers are calling, to implement unified credential management and sharing.
In this article, I'll share how I used Keyring to manage and share sign-in credentials of users. I hope this will help you.
Advantages
First, I'd like to explain some advantages of Keyring.
Building a seamless sign-in experience
Your app can call Keyring APIs to obtain sign-in credentials stored on user devices, for easy sign-in.
Ensuring data security and reliability
Keyring encrypts sign-in credentials of users for local storage on user devices and synchronizes the credentials between devices via end-to-end encryption technology. The encrypted credentials cannot be decrypted on the cloud.
Reducing the churn rate during sign-in
Keyring can simplify the sign-in process for your apps, thus reducing the user churn rate.
Reducing the operations cost
With Keyring, you can reduce the operations cost, such as the expense for SMS messages used by users to sign in to your app.
Development Procedure
Next, let's look at how to integrate Keyring. Before getting started, you will need to make some preparations, such as register as a Huawei developer, generate and configure your signing certificate fingerprint in AppGallery Connect, and enable Keyring. You can click here to learn about the detailed preparation steps, which will not be introduced in this article.
After making necessary preparations, you can now start integrating the Keyring SDK. I'll detail the implementation steps in two scenarios.
User Sign-in Scenario
In this scenario, you need to follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the Credential.getContent method to obtain the credential content and obtain the result from CredentialCallback<T>. Below is a code snippet example.
Code:
private Credential mCredential;
// Obtained credential.
mCredential.getContent(new CredentialCallback<byte[]>() {
@Override
public void onSuccess(byte[] bytes) {
String hint = String.format(getResources().getString(R.string.get_password_ok),
new String(bytes));
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
mResult.setText(new String(bytes));
}
@Override
public void onFailure(long l, CharSequence charSequence) {
Toast.makeText(MainActivity.this, R.string.get_password_failed,
Toast.LENGTH_SHORT).show();
mResult.setText(R.string.get_password_failed);
}
});
4. Call the credential saving API when a user enters a new credential, to save the credential. Below is a code snippet example.
Code:
AndroidAppIdentity app2 = new AndroidAppIdentity(sharedToAppName,
sharedToAppPackage, sharedToAppCertHash);
List<AppIdentity> sharedAppList = new ArrayList<>();
sharedAppList.add(app2);
Credential credential = new Credential(username, CredentialType.PASSWORD, userAuth,
password.getBytes());
credential.setDisplayName("user_niceday");
credential.setSharedWith(sharedAppList);
credential.setSyncable(true);
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
User Sign-out Scenario
Similarly, follow the steps below to implement relevant logic.
1. Initialize the CredentialClient object in the onCreate method of your activity. Below is a code snippet example.
Code:
CredentialClient credentialClient = CredentialManager.getCredentialClient(this);
2. Check whether a credential is available. Below is a code snippet example.
Code:
List<AppIdentity> trustedAppList = new ArrayList<>();
trustedAppList.add(new AndroidAppIdentity("yourAppName", "yourAppPackageName", "yourAppCodeSigningCertHash"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "www.yourdomain.com"));
trustedAppList.add(new WebAppIdentity("youWebSiteName", "login.yourdomain.com"));
SharedCredentialFilter sharedCredentialFilter = SharedCredentialFilter.acceptTrustedApps(trustedAppList);
credentialClient.findCredential(sharedCredentialFilter, new CredentialCallback<List<Credential>>() {
@Override
public void onSuccess(List<Credential> credentials) {
if (credentials.isEmpty()) {
Toast.makeText(MainActivity.this, R.string.no_available_credential, Toast.LENGTH_SHORT).show();
} else {
for (Credential credential : credentials) {
// Further process the available credentials, including obtaining the credential information and content and deleting the credentials.
}
}
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this, R.string.query_credential_failed, Toast.LENGTH_SHORT).show();
}
});
3. Call the deleteCredential method to delete the credential and obtain the result from CredentialCallback. Below is a code snippet example.
Code:
credentialClient.deleteCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
String hint = String.format(getResources().getString(R.string.delete_ok),
credential.getUsername());
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
String hint = String.format(getResources().getString(R.string.delete_failed),
description);
Toast.makeText(MainActivity.this, hint, Toast.LENGTH_SHORT).show();
}
});
Keyring offers two modes for sharing credentials: sharing credentials using API parameters and sharing credentials using Digital Asset Links. I will detail the two modes below.
Sharing Credentials Using API Parameters
In this mode, when calling the saveCredential method to save credentials, you can call the setSharedWith method to set parameters of the Credential object, to implement credential sharing. A credential can be shared to a maximum of 128 apps.
The sample code is as follows:
Code:
AndroidAppIdentity app1 = new AndroidAppIdentity("your android app name",
"your android app package name", "3C:99:C3:....");
QuickAppIdentity app2 = new QuickAppIdentity("your quick app name",
"your quick app package name", "DC:99:C4:....");
List<AppIdentity> sharedAppList = new ArrayList<>(); // List of apps with the credential is shared.
sharedAppList.add(app1);
sharedAppList.add(app2);
Credential credential = new Credential("username", CredentialType.PASSWORD, true,
"password".getBytes());
credential.setSharedWith(sharedAppList); // Set the credential sharing relationship.
credentialClient.saveCredential(credential, new CredentialCallback<Void>() {
@Override
public void onSuccess(Void unused) {
Toast.makeText(MainActivity.this,
R.string.save_credential_ok,
Toast.LENGTH_SHORT).show();
}
@Override
public void onFailure(long errorCode, CharSequence description) {
Toast.makeText(MainActivity.this,
R.string.save_credential_failed + " " + errorCode + ":" + description,
Toast.LENGTH_SHORT).show();
}
});
Sharing Credentials Using Digital Asset Links
In this mode, you can add credential sharing relationships in the AndroidManifest.xml file of your Android app. The procedure is as follows:
1. Add the following content to the <application> element in the AndroidManifest.xml file:
Code:
<application>
<meta-data
android:name="asset_statements"
android:value="@string/asset_statements" />
</application>
2. Add the following content to the res\values\strings.xml file:
Code:
<string name="asset_statements">your digital asset links statements</string>
The Digital Asset Links statements are JSON strings comply with the Digital Asset Links protocol. The sample code is as follows:
Code:
[{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "web",
"site": "https://developer.huawei.com" // Set your website domain name.
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "your android app package name",
"sha256_cert_fingerprints": [
"F2:52:4D:..."
]
}
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "quick_app",
"package_name": "your quick app package name",
"sha256_cert_fingerprints": [
"C3:68:9F:..."
]
}
}
]
The relation attribute has a fixed value of ["delegate_permission/common.get_login_creds"], indicating that the credential is shared with apps described in the target attribute.
And that's all for integrating Keyring. That was pretty straightforward, right? You can click here to find out more about Keyring and try it out.
Conclusion
More and more developers are prioritizing the need for a seamless sign-in experience to retain users and reduce the user churn rate. This is especially true for developers with multiple apps and app versions for different platforms, because it can help them share the user base of their different apps. There are many ways to achieve this. As I illustrated earlier in this article, my solution for doing so is to integrate Keyring, which turns out to be very effective. If you have similar demands, have a try at this service and you may be surprised.
Did I miss anything? Let me know in the comments section below.