I'm learning Google Play Billing Library to use in my Android App. I sell coin in my app as consumables product.
The flow is: user buy the item, app consume the item, if consume success increment the coin saved in Firestore.
What i want to ask is, what if increment coin saved in Firestore fails? Lets say because network or other things. This can cause a problem for users because our app has already consumed the item and users don't get their coins.
private fun handleConsumablePurchasesAsync(consumables: List<Purchase>) {
Timber.d("handleConsumablePurchasesAsync called")
consumables.forEach { purchase ->
Timber.d("handleConsumablePurchasesAsync foreach it is $purchase")
val params = ConsumeParams
.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
playStoreBillingClient.consumeAsync(params) { billingResult, purchaseToken ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchaseToken.apply { disburseConsumableEntitlements(purchase) }
}
else -> Timber.w(billingResult.debugMessage)
}
}
}
}
private fun disburseConsumableEntitlements(purchase: Purchase) {
when (purchase.sku) {
SkuKeys.COINS_5K -> addCoin(5000)
SkuKeys.COINS_10K -> addCoin(10000)
SkuKeys.COINS_100K -> addCoin(100000)
SkuKeys.COINS_500K -> addCoin(500000)
SkuKeys.COINS_1M -> addCoin(1000000)
SkuKeys.COINS_2M -> addCoin(2000000)
}
}
//what if this fails?
private fun addCoin(amount: Long) =
FirestoreRepository.incrementCoins(FirebaseAuthRepository.currentUserId, amount)
How to fix this problem? Are there any better approach?
My proposal,
Consume the item only after writing to Firebase successfully.
When the app is restarted and queryPurchases() returns that he still owns the item then try to update in Firebase again
I recommend save the purchases in to database once consumed , You can sync the data from database to firebase .
Once synced you can remove the data from database .
You can trigger a workmanager to sync the data additionally if required .(would be helpful if user kills the app)
This how ever is not secure as anyone with root access can get into database and manupulate with data . You can encrypt the database to make it little more secure but it wont be 100 % fool proof .
If you had a server then purchases could be validated on server side , but for you I can't think of any other solution .
Related
I'm trying to implement referral in my app with Firebase Dynamic link.
The happy path would be:
Link is shared with a JWT token as parameter
Another user clicks on the link, doesn't have the app so it goes to the Playstore
After app was installed, at app startup, the JWT token is retrieved trough parameters and stored locally
New signup is complete, the token is sent to the backend to activate referrer's reward
Problem is that in this scenario, step 3 doesn't find any data at first app startup, whereas it is found when I click on the link anew after the app has been installed.
This is the way the link is generated for the referrer:
val parameters = new DynamicLink.SocialMetaTagParameters.Builder()
.setImageUrl(...)
.setTitle(...)
.build();
val link = Uri.parse("https://my.domain.com/path/?token=${jwtToken}");
val dynamicLink = FirebaseDynamicLinks.getInstance().createDynamicLink()
.setLink(link)
.setSocialMetaTagParameters(parameters)
.setDomainUriPrefix("https://my.domain.com")
.setAndroidParameters(DynamicLink.AndroidParameters.Builder().build()))
.setIosParameters(...)
.setNavigationInfoParameters(
DynamicLink.NavigationInfoParameters.Builder()
.setForcedRedirectEnabled(true)
.build
).buildDynamicLink()
The link is shorten by:
FirebaseDynamicLinks.getInstance().createDynamicLink()
.setDomainUriPrefix("https://my.domain.com/")
.setLongLink(dynamicLink.getUri())
.buildShortDynamicLink()
.addOnCompleteListener(activity, task -> {
if (task.isSuccessful() && task.getResult() != null) {
linkCallback.success(task.getResult().getShortLink().toString());
} else {
linkCallback.failure(task.getException());
}
})
At app's opening, link is read in the onResume() method of the starting activity :
override fun onResume() {
super.onResume()
FirebaseDynamicLinks.getInstance()
.getDynamicLink(getIntent()) // getIntent() refers to the Activity's method
.addOnSuccessListener(activity) { data -> // Get deep link from result (may be null if no link is found)
val isReferralLink = data?.link?.toString()?.startsWith("https://my.domain.com/path") == true
val jwt = data?.link?.getQueryParameter("token")
// Here, data is null. <-------
// Other attempt :
data?.let { aiData ->
FirebaseAppInvite.getInvitation(aiData)?.let { result ->
// Here result is still null
}
}
}
}
I've seen on a stack overflow thread that it doesn't survive to beta track install, so I tried to leave the beta and use remote config to hide it in production track, but I have not been able see any difference.
And the version I'm using is :
implementation 'com.google.firebase:firebase-dynamic-links:21.0.0'
implementation 'com.google.firebase:firebase-analytics:20.0.0'
implementation 'com.google.firebase:firebase-invites:17.0.0'
Question:
Is there something I'm missing here ?
Thank you in advance for your help !
EDIT:
In this scenario the link and token are correctly found.
Link is shared with a JWT token as parameter
Another user clicks on the link, doesn't have the app so it goes to the Playstore
After app was installed, the second user clicks again on the link and opens the app for the first time
Ok so after a few days of testing and researches, I found out why it was not working.
FirebaseDynamicLinks.getInstance()
.getDynamicLink(getIntent())
.addOnSuccessListener(activity) { data ->
// Some data reading
}
In this case I was using the addOnSuccessListener() with an activity as parameter, which makes it lifecycle aware. In my app, a new user (fresh install) will be redirected to an onboarding activity, so the listener's activity is paused, and the callback is never fired.
=> TLDR: Removing this activity parameter solved my problem.
To begin with, I'm working on a Unity Game where I'm authenticating user when the game starts. My build environment is android. I'm using Firebase authentication for Google Play Games Services to authenticate user.
When the game starts in my android device or emulator, it is able to authenticate Play Games Services as well as able to connect with Firebase (I'm getting analytics data). However, when I pass the PlayGames AuthCode into Firebase.Auth Credentials, it stops executing the code (I've debug log for it). It does not throw any error in LogCat except
Firebase | server_auth_code
I tried searching web for different issues, but nothing. I checked my keys in player setting, firebase settings, OAuth 2.0 credentials on my Google API console and even check keys from my Google Play Console (which I'm not using at this stage). I have even checked my test users email addresses in Game Services and tried multiple google play games account. But issue still persist.
I'm using similar script in my other unity project where authentication works like a charm. I tried to use same script here and ended up with this issue: here. However, I solved it by removing all the packages and re-importing them into unity and changed my call functions in the script. Now, I'm stuck at this issue.
Here is cs file:
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using UnityEngine.SocialPlatforms;
using System.Threading.Tasks;
public class SetFirebase : MonoBehaviour
{
string authCode;
void Start()
{
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder().
RequestServerAuthCode(false /* Don't force refresh */).Build();
PlayGamesPlatform.InitializeInstance(config);
PlayGamesPlatform.Activate();
Social.localUser.Authenticate((bool success) =>
{
if (success)
{
authCode = PlayGamesPlatform.Instance.GetServerAuthCode();
Debug.Log("PlayGames successfully authenticated!");
Debug.Log("AuthCode: " + authCode);
}
else
{
Debug.Log("PlayGames SignIn Failed");
}
});
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
{
var dependencyStatus = task.Result;
if (dependencyStatus == Firebase.DependencyStatus.Available)
{
Debug.Log("Firebase Ready!!!");
RunFirebase();
}
else
{
Debug.LogError(System.String.Format("Could not resolve all Firebase dependencies: {0}", dependencyStatus));
}
});
}
private void RunFirebase(){
Firebase.Auth.FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
Debug.Log("init firebase auth ");
Firebase.Auth.Credential credential = Firebase.Auth.PlayGamesAuthProvider.GetCredential(authCode);
Debug.Log(" passed auth code ");
auth.SignInWithCredentialAsync(credential).ContinueWith(task =>
{
if (task.IsCanceled)
{
Debug.LogError("SignInOnClick was canceled.");
return;
}
if (task.IsFaulted)
{
Debug.LogError("SignInOnClick encountered an error: " + task.Exception);
return;
}
Firebase.Auth.FirebaseUser newUser = task.Result;
Debug.LogFormat("SignInOnClick: User signed in successfully: {0} ({1})", newUser.DisplayName, newUser.UserId);
});
}
}
My LogCat executes everything till "init firebase auth" but does not execute "passed auth code" so I know there is some issue in passing the credentials. It also does not run anything inside auth.SignInWithCredentialAsync(credential).
Any help or suggestion would be highly appreciated. Thank you.
There are two things I may suggest:
1) Replace ContinueWith with ContinueWithOnMainThread. This is a Firebase Extension that will guarantee that your logic runs on the main Unity thread (which tends to resolve many Unity specific issues). I go into more detail about that here.
2) Your logic may have a race condition between the Authenticate callback and the CheckAndFixDependenciesAsync continuation. These will not necessarily run in the order that you see them in your logic.
If I were building this system, I might prefer using Coroutines and a custom yield instruction:
class Authenticate : CustomYieldInstruction
{
private bool _keepWaiting = true;
public override bool keepWaiting => _keepWaiting;
public Authenticate(Social.ILocalUser user) {
user.Authenticate((bool success)=>{
/* old authentication code here */
_keepWaiting = false;
});
}
}
Then in a coroutine have something like:
private IEnumerator InitializeCoroutine() {
/* old authentication code */
// I'm ignoring error checking for now, but it shouldn't be hard to figure in.
// I'm mostly going from memory now anyway
// start both authentication processes in parallel
var authenticate = new Authenticate(Social.localUser);
var firebaseDependenciesTask = FirebaseApp.CheckAndFixDependenciesAsync();
// wait on social
yield return authenticate;
// wait on Firebase. If it finished in the meantime this should just fall through
yield return new WaitUntil(()=>firebaseDependenciesTask.IsComplete);
RunFirebase();
}
This way my logic looks roughly synchronous whilst still maintaining the asynchronosity (spell check claims that I made up that word) of the systems you're depending on and you avoid threading related issues that arise when using ContinueWith.
Let me know if that helps!
--Patrick
We have recently switched from Google Analytics SDK to Firebase SDK in our Android app.
Before that, we used INSTALL_REFERRER to get the user's source and medium.
Now we have started an App Campaign on Google Ads and INSTALL_REFERRER no longer works, though conversions keep coming.
How do we use Firebase SDK to know that user came from Google Ads campaign?
I think you'll want to use the Play Install Referrer API.
The link above cautions that the install referrer information will be available for 90 days and to only invoke the API during the first run of the app to avoid unnecessary API calls.
Here's an example (taking from the link above), assuming you have added the library to your build.gradle file:
Initialization:
private lateinit var referrerClient: InstallReferrerClient
...
referrerClient = InstallReferrerClient.newBuilder(this).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
when (responseCode) {
InstallReferrerResponse.OK -> {
// Connection established
}
InstallReferrerResponse.FEATURE_NOT_SUPPORTED -> {
// API not available on the current Play Store app
}
InstallReferrerResponse.SERVICE_UNAVAILABLE -> {
// Connection could not be established
}
}
}
override fun onInstallReferrerServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
Getting the referrer:
val response: ReferrerDetails = referrerClient.installReferrer
val referrer = response.installReferrer
val clickTimestamp = response.referrerClickTimestampSeconds
val installTimestamp = response.installBeginTimestampSeconds
Wrapping Up:
referrerClient.endConnection()
Checking for gclid (Google Ads)
if ("gclid" in referrer) {
//report to Firebase Analytics
} else {
//do something else
}
In this way, we will determine gclid only if the user has clicked on the advertisement in the browser, but if he clicked on the advertisement in the play market, then nothing will work.
I'm building an android application in which the user can choose a playlist from youtube and after choosing it I will need to load all the videos in that playlist into a single list.
I found several solutions online but they were using javascript and the ones that were written in rxjava the paginate was using int numbers rather then strings (I won't be able to use range).
For now I'm able to load the first 50 videos, I tried several approaches (one of which was a recursion but apparently I wrote it bad and I ended up using all the api requests in one load).
Here is my code:
fun getVidoesFromPlayList(playListId: String,pageToken: String): Single<MutableList<YTVideo>> {
return youTubeService.getPlayListResponse(playListId, pageToken)
.flatMap { response ->
Observable.just(response.items)
.flatMapIterable { data -> data }
.flatMap { item ->
youTubeService.getVideoResponse(item.snippet!!.resourceId!!.videoId).toObservable()
.subscribeOn(Schedulers.io())
.map { videoResponse ->
YTVideo(
item.snippet.position,
item.snippet.resourceId!!.videoId,
item.snippet.thumbnails!!.medium!!.url,
item.snippet.title,
videoResponse.items!![0].contentDetails!!.duration,
videoResponse.items[0].snippet!!.channelTitle
)
}
}
.repeatUntil { response.nextPageToken!=null }
.toList()
}
}
1) How can I call to getVidoesFromPlayList with a different nextpagetoken while caching the current videos?
2) Is there a way to do it more effecently without using recursion?
I will appreciate any help or guidance, thank you!
I'm currently trying to integrate the Google Consent SDK.
something is happening that I don't understand with the onConsentInfoUpdated(consentStatus:ConsentStatus) function
User opens the app for the first time and makes a choice when the popup is diplayed
Consent.Status -> UNKNOWN
User kills / re-opens the app.
Sometimes Consent.Status -> PERSONALIZED or Consent.Status -> UNKNOWN and user makes a choice again.
here my code:
fun requestConsent(activity: Activity) {
val consentInformation = ConsentInformation.getInstance(activity)
val publisherIds = arrayOf(activity.getString(R.string.admob_publisher_id))
consentInformation.requestConsentInfoUpdate(publisherIds, object : ConsentInfoUpdateListener {
override fun onConsentInfoUpdated(consentStatus: ConsentStatus) {
Log.d("test--", consentStatus.toString())
when (consentStatus) {
ConsentStatus.PERSONALIZED -> showPersonalizedAds()
ConsentStatus.NON_PERSONALIZED -> showNonPersonalizedAds()
ConsentStatus.UNKNOWN -> loadConsentForm(activity)
}
}
override fun onFailedToUpdateConsentInfo(errorDescription: String) {
//onFailedToUpdateConsentInfo()
}
})
}
private fun showPersonalizedAds() {
ConsentInformation.getInstance(activity).consentStatus = ConsentStatus.PERSONALIZED
}
private fun showNonPersonalizedAds() {
ConsentInformation.getInstance(activity).consentStatus = ConsentStatus.NON_PERSONALIZED
}
here my logs:
This is the first result that came up on Google and I tried your solution but it didn't resolve the issue. In my case I was using:
com.google.android.ads.consent:consent-library:1.0.6
The issue was resolved by upgrading to:
com.google.android.ads.consent:consent-library:1.0.8
Google removed the restriction on using Google rendered consent form with the commonly used set of ad technology providers in 1.0.6 and changed logic for re-consent prompts in 1.0.8 - as seen here: https://github.com/googleads/googleads-consent-sdk-android/blob/master/ChangeLog.md
Could it be that you also changed your library version without realising?
by default, all advertisers are selected in the admob console. That's why I was able to solve my problem by going to the blocking settings section for users in the European Union.
click Blocking controls and then EU user consent
https://support.google.com/admob/answer/7666519#providers