So I've implemented IAP in Android using Unity but one thing remains is how to validate a user across multiple devices.
For instance, user purchases a consumable on one phone and then logs in on another phone...
How can I validate the user has made a purchase on the other phone? The purchase is tied to the google account and not the phone. So is there a way to get info about the google account that is logged in currently? Then I could save the receipt behind the scenes and verify when the user logs in on the other phone against a server. But without some unique identifier across phones I would have to force the user to create an account which I do not want to do.
I am currently developing an iOS/Android game. The approach I took to this issue was utilizing the Cloud Save feature offered by Google/Apple. When saving/loading data, there is the chance that there is a conflict with the local vs. cloud data. When there is a conflict, it is here where you will be able to merge data between the local and remote data.
There is a decision at least for Google to choose between the callback resolution of automatically resolving the conflict using one of their default conflict resolutions (longest playtime, newest data, etc.) or manually resolving the conflict. You will want to manually resolve the conflict so you can merge the data together from both saves.
What is important is how you now save this data to the cloud and how you serialize purchases. I will show how I am handling this, but you can approach it in a different way.
[System.Serializable]
public class IndividualPurchaseDataSave
{
public IndividualPurchaseDataSave(string id, bool used)
{
purchaseID = id;
isApplied = used;
}
public string purchaseID = "";
public bool isApplied = false;
}
/// <summary>
/// All purchase data
/// </summary>
[System.Serializable]
public class PurchaseDataSave
{
public List<IndividualPurchaseDataSave> PurchaseData = new List<IndividualPurchaseDataSave>();
}
I have two structures, the first being an individual purchase where the second structure holds a list of all purchases that can be serialized in JSON to push to the cloud.
When the user buys a new in-app purchase, after receiving the Google callback confirmation, I call this function with the purchaseID.
/// <summary>
/// Purchases an item and adds it to the dictionary for later usage
/// </summary>
/// <param name="purchaseID"></param>
public void PurchaseItem(string purchaseID)
{
// adding a delimeter of | to split it later in case we need to handle merging data
PlayerInAppPurchaseHistory.Add(purchaseID + "|" + System.DateTimeOffset.Now.ToUnixTimeMilliseconds(), true);
}
The dictionary structure looks as follows:
private Dictionary<string, bool> PlayerInAppPurchaseHistory = new Dictionary<string, bool>();
I am adding the purchaseID along with the time purchased in milliseconds as that should be unique enough to never occur again. Wrapping it in a delimiter allows me to still split the string if I need the purchaseID for other processing.
When merging the data together, you receive a byte[] from Google and need to convert this data back to something of use. Once you are able to receive the remote data purchase data, you can merge them using a HashSet.
// merge the remote INTO the local
HashSet<string> purchaseData = new HashSet<string>();
// add our existing purchases to a hashset
foreach (IndividualPurchaseDataSave data in localPurchaseData.PurchaseData)
{
purchaseData.Add(data.purchaseID);
}
// now iterate over our remote purchases and if anything is missing, add it to our temp data
foreach (IndividualPurchaseDataSave data in remotePurchaseDate.PurchaseData)
{
// purchase does not exist, so add it and set it as not applied (false)
if (!purchaseData.Contains(data.purchaseID))
{
localPurchaseData.PurchaseData.Add(new IndividualPurchaseDataSave(data.purchaseID, false));
}
}
After merging the data, you can cache which values are added, but I am reloading the scene as my merge conflict resolution is a bit more complex, so I am adding the new purchases in the Load
foreach(IndividualPurchaseDataSave purchaseData in data.PurchaseData)
{
// if the data is not processed, then process it now - split it over the time
if (!purchaseData.isApplied)
ProcessPurchaseItem(purchaseData.purchaseID.Split('|')[0], false);
// add it to our dictionary
PlayerInAppPurchaseHistory.Add(purchaseData.purchaseID, true);
}
Effectively storing the data in some identifiable way that can be merged at a later time is how you will want to approach this issue. Using cloud saving has no additional cost to me, so that is why I decided to utilize the cloud for this issue. This code is not functional as is, it should be used as a guide.
Related
I'm new in Android and Firestore. Im currently making an app that uses Cloud Firestore. In my transaction I have one read (transaction.get()) and one write (transaction.set()) operation. I've noticed that in usage tab in Firebase Console, this transaction increments read counter by 1, but write counter increments by 2. I have removed transaction.set operation for testing, and with transaction.get operation only, this whole transaction still increments write counter by 1. Is it normal? Are those normal rules for billing transactions in firestore? I don't know if it matters that reading and writing is done to different files in Cloud Firestore.
db.runTransaction(new Transaction.Function<Object>() {
#Nullable
#Override
public Object apply(#NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction.get(carReference);
.
.
.
transaction.set(pointReference, point);
return null;
}
});
According to the Official Documentation, You are charged for each document read, write, and delete that you perform with Cloud Firestore.
In your case, I am not able to see why the write is incremented by 2 the first time you write. Maybe you are writing something else in the code.
But regarding the reads, it's an expected behavior because when you listen to the results of a query, you are charged for a read each time a document in the result set is added or updated. And in your case, as you are setting, so the second time, the read is incremented by 1.
Got this basic Firebase RemoteConfig A/B-Test running on Android. I want to get the title/name and description of the A/B-Test configurated in Firebase. Also it would be nice to get the name of the variations (Control, Variation A, ...)
How do I get these data?
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// bind XML elements into variables
bindWidgets();
// Only for debugging: get Instance ID token from device
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
#Override
public void onComplete(#NonNull Task<InstanceIdResult> task) {
String deviceToken = task.getResult().getToken();
Log.wtf("Instance ID", deviceToken);
}
});
// Remote Config Setting
FirebaseRemoteConfigSettings mFirebaseRemoteConfigSettings = new FirebaseRemoteConfigSettings
.Builder()
.setDeveloperModeEnabled(BuildConfig.DEBUG)
.build();
mFirebaseRemoteConfig.setConfigSettings(mFirebaseRemoteConfigSettings);
// Remote Config with HashMap
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("buttonColor", "#999999");
mFirebaseRemoteConfig.setDefaults(hashMap);
final Task<Void> fetch = mFirebaseRemoteConfig.fetch(FirebaseRemoteConfig.VALUE_SOURCE_STATIC);
fetch.addOnSuccessListener(this, new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
mFirebaseRemoteConfig.activateFetched();
// get value of key buttonColor from HashMap
String buttonColor = mFirebaseRemoteConfig.getString("buttonColor");
button.setBackgroundColor(Color.parseColor(buttonColor));
}
});
}
There is no official API to retrieve any information about your A/B test, besides the variant selected.
It's going to be much, much easier to just hardcode the values inside your app, or manually add them on Firebase Hosting / Cloud Firestore.
That being said, here's 2 vague ideas for more automatic solutions, but I really don't recommend trying either!
BigQuery
You could link your project to BigQuery, it will then contain your Analytics data. Specifically:
In this query, your experiment is encoded as a user property with the experiment name in the key and the experiment variant in the value.
Once your data is in BigQuery, you could then retrieve it using the SDKs. You will of course need to handle permissions and access control, and this is almost certainly extremely overkill.
Cloud Functions (and hosting)
Another solution is to just store the data you need elsewhere, and retrieve it. Firebase Cloud Functions have the ability to react to a new remote config (A/B tests use these under the hood). So you could create a function that:
Is triggered on new remote config creation.
Stores a mapping of parameter key to name etc in Cloud Firestore or similar.
Your app could then query this Cloud Firestore / hosted file / wherever you hosted it.
Note: I couldn't actually figure out how to get any info about the remote config in Cloud Functions. Things like version name, update time etc are available, but description seems suspiciously vague.
We wanted to get these informations for our Tracking/Analyzing Tools. So we implemented a workaround and added an additional Remote Config variable abTestName_variantInfo where we set a short info in the A/B-Testing configuration about the name of the A/B-Test and the variant we are running in. With this we can use the main Remote Config variable for the variant changes (e.g layout or functionality) without being dependent of our own naming-convention for tracking.
For example we used the two Remote Config variables ratingTest_variant (values: emojis or stars) and added the variable ratingTest_variantInfo (values: abTest_rating_emojis and abTest_rating_stars).
I created something similar to a subscription/like counter using Firebase Real-time Database and Firebase's cloud functions (using a transaction):
// This is a cloud function that increases subs by 1
export const onSubscriberCreate = functions.database
.ref('/channels/{$ch_id}/subscribers/{$uid}')
.onCreate((snapshot, context) => {
const countRef = snapshot.ref.parent.parent.child('subs_count')
return countRef.transaction(count => {
return count + 1
})
})
Then, I used FirebaseUI (FirebaseRecyclerAdapter) for Android to populate a RecyclerView of channels. When the user presses a channel's "Subscribe" button, his id is being sent to /channels/{$ch_id}/subscribers/ which triggers the cloud function.
However, the cloud function is really slow (about 5 secs), so I want to "fake" the update of the counter displayed to the user even before the cloud function is executed (I tried it by changing the TextView):
channelRef.child("subscribers")
.child(user.getUid()).setValue(true)
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
subsInfo.setText(channel.getSubs_count() + 1) + " SUBSCRIBERS");
}
});
The problem is that the channel object is being updated twice (the user id on the subscribers' list and the counter increased) so that the information of the channel is being downloaded again and binded to the ViewHolder even before the server updates the counter, so that's not a good solution.
I thought about moving the transaction code into the client, is it really necessary? Is there a better solution?
The best thing I feel you should do is to move the subscriber's list from inside the channel node to somewhere out. This will also make your channel object weigh lesser and you can store/update number of subscribers easily inside the channel node. Now for every user, you are downloading the entire list of users everytime you want to download a channel's information. And you don't need a cloud function to update the number of subscribers. You can do that totally on the client side using Transactions.
root/channels/{$channel}/{channelName,numberOfSubscribers,etc}
root/subscribers/{&channel}/{$userId}
This is probably how you want your data structure should be unless you really want to get all the users list. If that's the case, you can just show the size of the list of subscribers inside the TextView where you are showing the number of subscribers.
I'm keeping track of a count that users update on the Firebase database through an Android app. The way it works right now is that upon interaction the user's app looks up the current count on the database (using a addListenerForSingleValueEvent() and onDataChange() method defined within the new ValueEventListener) and adds one to it and then sets the count to this new value using mRef.setValue() where mRef is the reference to the database.
The issue I'm worried about is what would happen if a large number of users interacted with the database together at the same time; does Firebase take care of making sure that the value is read and incremented properly or is there a lot of overlap and potentially a loss of data because of that.
When working with complex data that could be corrupted by concurrent modifications, such as incremental counters, Firebase provides a transaction operation.
You give this operation two arguments: an update function and an optional completion callback. The update function takes the current state of the data as an argument and will return the new desired state you would like to write.
For example, if we wanted to increment the number of upvotes on a specific blog post, we would write a transaction like the following (Legacy code):
Firebase upvotesRef = new Firebase("https://docs-examples.firebaseio.com/android/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes");
upvotesRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData currentData) {
if(currentData.getValue() == null) {
currentData.setValue(1);
} else {
currentData.setValue((Long) currentData.getValue() + 1);
}
return Transaction.success(currentData); //we can also abort by calling Transaction.abort()
}
#Override
public void onComplete(FirebaseError firebaseError, boolean committed, DataSnapshot currentData) {
//This method will be called once with the results of the transaction.
}
});
Legacy source
New firebase version source
Firebase database handles up to 100 simultaneous real time connections to your database if your are using their free plan but once the 101st users connects to your database the database would stop responding and would display the values that were last edited. Firebase is really good at handling real time connections simultaneously so it depends on your pricing plans. If you want to use the database for free, there will be no issues handling 100 connections but if you want to handle more users use their generous pricing plans.
I am using firebase geofire library to fetch key's based on location but because of thousands of keys are returned in onKeyEntered() event every time I have to take the key refer firebase and get the object back and set it to listview becoming very slow. I tried commenting all other work in onKeyEntered() to see how fast geofire is then I was surprised withing 900 milli's all callbacks received.
So now what is the best optimized way to get the data from firebase using the key passed in onKeyEntered() callback and set it to listview so that even for thousands of entries listview should load fast
I thought of AsyncTask in every callback give the fetching data work to AsyncTask and proceed with next callback key and do same, but not sure thats correct.
Or load only few and then load as scroll's is also good idea but geofire returns keys from all over the database so there is no option to get only few latest one so not sure how to implement it.
This is what I am trying but list view loads very slow.
#Override
public void onKeyEntered(String key, GeoLocation location) {
Log.d("geoevent", key);
mDatabaseTemp = FirebaseDatabase.getInstance().getReference("/posts/" + key);
mDatabaseTemp.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
Post post = new Post();
post = dataSnapshot.getValue(Post.class);
mPosts.add(post);
mPostAdapter.notifyDataSetChanged();
mRecycler.smoothScrollToPosition(mPosts.size() - 1);
}
#Override
public void onCancelled(DatabaseError databaseError) {
Toast.makeText(getActivity(), "error" + databaseError, Toast.LENGTH_LONG).show();
}
});
}
Question of the type "how to best" are notoriously difficult to answer. But I'll give a few hints about the general behavior of the system that you may not be aware of.
the Firebase Database client interacts with the network and disk on a separate thread. When data is available for your client, it calls your handlers on the main/UI thread. This means that putting your calls in an AsyncTask is only useful if you'll be doing significant work in the callback.
the Firebase Database client pipelines the calls to the server over a single connection. To read more about why that affects performance, see Speed up fetching posts for my social network app by using query instead of observing a single event repeatedly.
loading thousands of items into a mobile device over the network is in general not a good idea. You should only load data that you'll show to the user and thousands of items is well beyond what can be reasonably presented on a mobile screen. When developing a location based app, you could show a map where the user indicates a hotspot and you use Geoqueries to only request items around that spot.