I have successfully implemented In-App-Billing into my app. The IAP is working successfully and I have tested it to make sure its working.
When a user clicks on a button, they have to make an IAP to proceed. However, everytime a user clicks on the button it starts the IAP, even though they have already made the IAP. I want my IAP to be non-consumable obviously. Currently, I'm storing the IAP in SharedPreferences, but if the user reinstalls the app, they lose their IAP.
So how can I use getPurchases() or restoreTransactions() on my onCreate or onClick method to check whether the user has purchases a specific item? I have searched all over the Internet and read through so many samples and it doesn't seem to work, perhaps I am misunderstanding though.
If you need me to post any code, please ask and I'll update my post.
Use this library:
https://github.com/anjlab/android-inapp-billing-v3
How to use?
Use this in your gradle:
repositories {
mavenCentral()
}
dependencies {
implementation 'com.anjlab.android.iab.v3:library:1.0.44'
}
Manifest permission for in-app billing:
<uses-permission android:name="com.android.vending.BILLING" />
How to use library method:
public class SomeActivity extends Activity implements BillingProcessor.IBillingHandler {
BillingProcessor bp;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bp = new BillingProcessor(this, "YOUR LICENSE KEY FROM GOOGLE PLAY CONSOLE HERE", this);
bp.initialize();
// or bp = BillingProcessor.newBillingProcessor(this, "YOUR LICENSE KEY FROM GOOGLE PLAY CONSOLE HERE", this);
// See below on why this is a useful alternative
}
// IBillingHandler implementation
#Override
public void onBillingInitialized() {
/*
* Called when BillingProcessor was initialized and it's ready to purchase
*/
}
#Override
public void onProductPurchased(String productId, TransactionDetails details) {
/*
* Called when requested PRODUCT ID was successfully purchased
*/
}
#Override
public void onBillingError(int errorCode, Throwable error) {
/*
* Called when some error occurred. See Constants class for more details
*
* Note - this includes handling the case where the user canceled the buy dialog:
* errorCode = Constants.BILLING_RESPONSE_RESULT_USER_CANCELED
*/
}
#Override
public void onPurchaseHistoryRestored() {
/*
* Called when purchase history was restored and the list of all owned PRODUCT ID's
* was loaded from Google Play
*/
}
}
Note: onPurchaseHistoryRestored called only first time when you initialize BillingProcessor
Their are lot of ways to check onPurchaseHistoryRestored() but its my opinion. And I have solved it by using..
you can check through transection detail using this code.
#Override
public void onPurchaseHistoryRestored() {
/*
* Called when purchase history was restored and the list of all owned PRODUCT ID's
* was loaded from Google Play
*/
// Check whether 'premium_id' has previously been purchased:
TransactionDetails premiumTransactionDetails = bp.getPurchaseTransactionDetails("premium_id");
if (premiumTransactionDetails == null) {
Log.i(TAG, "onPurchaseHistoryRestored(): Havn't bought premium yet.");
purchasePremiumButton.setEnabled(true);
}
else {
Log.i(TAG, "onPurchaseHistoryRestored(): Already purchases premium.");
purchasePremiumButton.setText(getString(R.string.you_have_premium));
purchasePremiumButton.setEnabled(false);
statusTextView.setVisibility(View.INVISIBLE);
}
}
And the second is to put the check on "product id".check below code.
if(bp.isPurchased(REMOVE_ID_SKU)){
if (bp.loadOwnedPurchasesFromGoogle()) {
Toast.makeText(this, R.string.subs_updated, Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(this, R.string.no_purchases, Toast.LENGTH_SHORT).show();
}
}
Related
I've integrated UnityAds on my Android app (that is not published yet).
I get app id and placement id from database on my server.
App id and placement id are correct, I've copied and pasted about 30 times for be sure of it.
So, when I try to get an ad in test mode, it give me the INVALID_ARGUMENT error.
Here an explaination of the error code by Unity, but as you can see it is a little generic.
I have an object that simply represents an ad service (like admob, FAN, inmobi etc)
In this case the object is called advert, and here it's how I show an ad with Unity:
protected void showUnity(){
UnityAds.initialize(this, advert.getApiKey(), true); //advert.getApiKey() returns the app id
UnityAds.addListener(new IUnityAdsListener() {
#Override
public void onUnityAdsReady(String s) {
Log.i(TAG, "onUnityAdsReady "+s);
if(s.equals(advert.getUnitId()) && !unityReady)
UnityAds.show(ActivityAd.this, advert.getUnitId()); //advert.getUnitId() returns the placement id
}
#Override
public void onUnityAdsStart(String s) {
Log.i(TAG, "onUnityAdsStart "+s);
unityReady = true;
}
#Override
public void onUnityAdsFinish(String s, UnityAds.FinishState finishState) {
if (finishState.compareTo(UnityAds.FinishState.COMPLETED) == 0) {
onAdReward(); //my callback for reward
} else if (finishState.compareTo(UnityAds.FinishState.SKIPPED) == 0) {
onAdClosed(); //my callback for ad close
} else if (finishState.compareTo(UnityAds.FinishState.ERROR) == 0) {
onAdError(finishState.toString()); //my callback for errors
}
}
#Override
public void onUnityAdsError(UnityAds.UnityAdsError unityAdsError, String s) {
onAdError(unityAdsError.toString()); //my callback for errors, here results INVALID_ARGUMENT error
}
});
}
Does anyone know what is wrong? Thanks in advance
If you check the callback closely the onUnityAdsError has 2 params, first provides the error code and the second param provides you information about what went wrong.
#Override
public void onUnityAdsError(UnityAds.UnityAdsError unityAdsError, String reason) {
onAdError(unityAdsError.toString()); //my callback for errors, here results INVALID_ARGUMENT error
}
So just check the reason and you should be able to find out what is going wrong in your integration.
Here are some methods which you can follow to solve this INVALID_ARGUMENT problem
1. Make sure you are implementing the right Initialization code in your app. There are 2 types of Initialization.
Only Unity ads Initialization
Mediation Initialization
and both methods have their own banner, interstitial, and rewarded ad code.
2. Make sure you enable test mode as Boolean. (i.e: private Boolean testMode = true;) (make sure to do false this before publish on store)
3. You can add your mobile phone as a test device to get test ads on your phone forcefully. for this, you have to first copy the Ad ID of your device. For that, go to your mobile settings > Google > Ads > This device's advertising ID. copy that ID and go to unity dashboard > Monetization > Testing > Add Test Device. Add your device Ads ID here with any name, and now you will be able to see test ads on the device.
I followed the guide on the Android docs but for some reason nothing is showing when i start my app.
I even tried logging the listeners but nothing is showing up in logcat.
I also changed the ad technology in admob setting to Custom set of ad technology providers, but still not working.
My code
ConsentInformation consentInformation = ConsentInformation.getInstance(getApplicationContext());
ConsentInformation.getInstance(getApplicationContext()).addTestDevice("6AE7D8950FE9E464D988F340C0D625B0");
ConsentInformation.getInstance(getApplicationContext()).
setDebugGeography(DebugGeography.DEBUG_GEOGRAPHY_EEA);
String[] publisherIds = {""};
consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() {
#Override
public void onConsentInfoUpdated(ConsentStatus consentStatus) {
// User's consent status successfully updated.
Log.d(TAG,"onConsentInfoUpdated");
}
#Override
public void onFailedToUpdateConsentInfo(String errorDescription) {
// User's consent status failed to update.
Log.d(TAG,"onFailedToUpdateConsentInfo");
}
});
form = new ConsentForm.Builder(this, privacyUrl)
.withListener(new ConsentFormListener() {
#Override
public void onConsentFormLoaded() {
// Consent form loaded successfully.
Log.d(TAG,"form loaded!");
form.show();
}
#Override
public void onConsentFormOpened() {
// Consent form was displayed.
}
#Override
public void onConsentFormClosed(
ConsentStatus consentStatus, Boolean userPrefersAdFree) {
// Consent form was closed.
}
#Override
public void onConsentFormError(String errorDescription) {
// Consent form error.
Log.d(TAG,"form error!");
}
})
.withPersonalizedAdsOption()
.withNonPersonalizedAdsOption()
.withAdFreeOption()
.build();
form.load();
Gradle
dependencies {
classpath 'com.google.gms:google-services:4.3.2'
}
implementation 'com.google.android.ads.consent:consent-library:1.0.7'
implementation 'com.google.android.gms:play-services-plus:17.0.0'
implementation 'com.google.android.gms:play-services-ads:18.2.0'
EDIT
I tried it on a project which was pre android x and now it calls the listener onFailedToUpdateConsentInfo.
With following error message:
onFailedToUpdateConsentInfoCould not parse Event FE preflight response.
Searched a bit and found this could be because of an invalid pub id, but i'm certain i'm using the right one.
1) I think you forget to check isRequestLocationInEeaOrUnknown() method.
It will return true If user already agreed to the consent. In this case, you don't need to ask it again. I think you already agreed to consent.
wrap your code with
if(ConsentInformation.getInstance(context).isRequestLocationInEeaOrUnknown()){
//setup admob
}else{
//Ask for consent
}
2) You have to call form.show(); to present the form to the user, check Google Doc
I was still using test app id and test ad ids, remove them and change it with your id's and make sure you use it as a testdevice so you don't violate admob policies.
Like this
adLoader.loadAd(new AdRequest.Builder().addTestDevice(AdRequest.DEVICE_ID_EMULATOR).build());
I'm running this code with a Twitter handle I'm pretty sure doesn't exist in order to test error handling. The breakpoints on the Callback are never hit, neither for success nor failure.
Any pointers on why this is?
Just as a note, this code works fine with a valid Twitter handle, but doesn't call the Callback either.
final Callback<Tweet> actionCallback = new Callback<Tweet>() {
#Override
public void success(Result<Tweet> result) {
int x = 1;
x++; // This code is just so I can put a breakpoint here
}
#Override
public void failure(TwitterException exception) {
DialogManager.showOkDialog(context, R.string.twitter_feed_not_found);
}
};
final UserTimeline userTimeline = new UserTimeline.Builder().screenName(handleStr + "dfdfddfdfdfasdf") // Handle that doesn't exist
.includeReplies(false).includeRetweets(false).maxItemsPerRequest(5).build();
final TweetTimelineListAdapter adapter = new TweetTimelineListAdapter.Builder(context)
.setTimeline(userTimeline)
.setViewStyle(R.style.tw__TweetLightWithActionsStyle)
.setOnActionCallback(actionCallback)
.build();
listView.setAdapter(adapter);
I think you misundestood the purpose of the actionCallback. From the source code of the TweetTimelineListAdapter you can see that this callback is for the actions on tweet view,ie, when you click on favorite icon for example. I've test with the favorite icon and the callback gets called.
Take a look at this comment at the getView method of the source code.
/**
* Returns a CompactTweetView by default. May be overridden to provide another view for the
* Tweet item. If Tweet actions are enabled, be sure to call setOnActionCallback(actionCallback)
* on each new subclass of BaseTweetView to ensure proper success and failure handling
* for Tweet actions (favorite, unfavorite).
*/
The callback is not intended to handle a screenname that does not exist and indeed the actions/buttons of a specific tweet.
Hope this helps.
UPDATED: You don't need to detect any erros on UserTimeLine, since the builder does not throw any exception and the adapter will be empty, with no rows/views showing on the screen. But if you still need to detect some "error" in the loading you have to rely on the "next" method of the UserTimeLine.
Take a look
userTimeline.next(null, new Callback<TimelineResult<Tweet>>() {
#Override
public void success(Result<TimelineResult<Tweet>> result) {
}
#Override
public void failure(TwitterException exception) {
Log.d("TAG",exception.getMessage());
}
});
This method shows the next tweet for the user, if the failure callback get called you will know for sure that this user does not have any tweet or the user does not exist.
I've been struggling with this problem for days now. I know there are a lot of questions with the same problem on SO but i couldn't get it to work.
What I have done
Uploaded APK in beta phase
Created Merchant account
Added test user
Code
AndroidManifest.xml
<uses-permission android:name="com.android.vending.BILLING" />
MainActivity.java
public class MainActivity extends AppCompatActivity {
private IabHelper mHelper;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ...
setupInAppBillings();
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
// [....]
private void setupInAppBillings() {
String base64EncodedPublicKey = "MY PUBLIC KEY";
mHelper = new IabHelper(this, base64EncodedPublicKey);
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
Toast.makeText(getContext(), "In-app Billing setup failed: " + result, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "In-app Billing is set up OK", Toast.LENGTH_SHORT).show();
}
}
});
}
}
Tested on
Huawei P8 (Google Play Version 6.2.14)
In Switzerland, so a supported country for In-App Billing
What I've tried
Deleted cache and data from Google Play
Tutorial from Google Developer site
Went trough the checklist from user sokie: answer of sokie
The only thing I haven't done from this list is the setup of the License Verification Library (LVL). But I couldn't find any information that this step is required for an In-App Purchase. If not needed I want to do it without this library because I don't really need it as stated on the Google Site.
The Google Play Licensing service is primarily intended for paid applications that wish to verify that the current user did in fact pay for the application on Google Play.
Is there something I miss?
if you target android 31 you should add this to your manifest :
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
Finally I got it to work! The problem was the following: Even though I put the IInAppBillingService.aidl in the com.android.vending.billing package, the generated class was in the wrong package as you can see in the code below.
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: C:\\path\\src\\main\\aidl\\com\\android\\vending\\billing\\IInAppBillingService.aidl
*/
package billing;
public interface IInAppBillingService extends android.os.IInterface { //... }
To solve this, I deleted and recreated the com.android.vending.billing package with the IInAppBillingService.aidl. So if you have the same problem, check twice where the IInAppBillingService.java was generated.
I recently faced this problem. As Bona Fide wrote, the package declaration in IInAppBillingService.aidl must be set to "com.android.vending.billing" and the aidl file should be found inside the corresponding directory using the explorer. Furthermore (and that was the problem in my case), in the IabHelper.java, the string parameter to serviceIntent must be the same as the package name that contains the IInAppBillingService.aidl file.
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");// correct package name: com.android.vending.billing
serviceIntent.setPackage("com.android.vending");
List<ResolveInfo> intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
if (intentServices != null && !intentServices.isEmpty()) {
// service available to handle that Intent
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}
else {
// no service available to handle that Intent
if (listener != null) {
listener.onIabSetupFinished(
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
}
}
on the weekend I started to build my first android app. As I need to ask the user of my app for user credentials [which are used for further webservice usage] I want to simulate a "login system". On the start of my app the user should be told to enter his credentials. When the user is inactive for too long I want to dismiss the entered credentials and to "log out" the user.
While coding and afterwards while testing I realized that the way I thought I could go doesn't work. After reading the docu and several SO-questions again and again I question myself more and more if I have understand the app / activity life cycle and its possibilites fully. So I wanted to ask for help in understand the life cycle and its linked influences on my app. So yes this might be several questions in one :/
For the moment my app consists of the following activities:
a search activity (which is opened once the app is started)
a settings acitivy (which can be accessed from the search dialog and has a link back to the search dialog)
After the user has entered an ID in the search dialog I want to open an activity regarding to the search result (NYI).
When starting to implement the user auth, my idea was the following:
Everytime onResume() of an activity is called I need to check a) if user credentials are already stored and b) if the last action of the user is less then X minutes ago. If one these fails I want to show a "log in panel" where the user can enter his credentials, which are then stored in the SharedPreferences. For that I did the following:
I first build an parent activity which has the check and a reference for the SharedPreferences in it
public class AppFragmentActivity extends FragmentActivity {
protected SharedPreferences sharedPref;
protected SharedPreferences.Editor editor;
protected String WebServiceUsername;
protected String WebServicePassword;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_appfragmentactivity);
}
#Override
protected void onResume () {
super.onResume();
// Check if user is "logged in".
// Meaning: Are there given user credentials and are they valid of was the user inactive for too long?
// We only do this "onResume" because this callback is the only one, which is called everytime an user
// starts/restarts/resumes an application
checkForUserCredentials();
// Set new "last action" now "now"
setLastAction(new Date().getTime());
}
#Override
protected void onStart () {
// Fill content
super.onStart();
// Set global sharedPreferences
sharedPref = this.getSharedPreferences(getString(R.string.FILE_settings_file), Context.MODE_PRIVATE);
}
/*
* Checks if user credentials are valid meaning if they are set and not too old
*/
private void checkForUserCredentials() {
long TimeLastAction = sharedPref.getLong(getString(R.string.SETTINGS_USER_LAST_ACTION), 0);
long TimeNow = new Date().getTime();
// Ask for User credentials when last action is too long ago
if(TimeLastAction < (TimeNow - 1800)) {
// Inactive for too long
// Set back credentials
setUsernameAndPassword("", "");
}
else
{
WebServiceUsername = sharedPref.getString(getString(R.string.SETTINGS_USER_USERNAME), "");
WebServicePassword = sharedPref.getString(getString(R.string.SETTINGS_USER_PASSWORD), "");
}
}
/*
* Saves the given last action in the sharedPreferences
* #param long LastAction - Time of the last action
*/
private void setLastAction(long LastAction) {
editor = sharedPref.edit();
editor.putLong(getString(R.string.SETTINGS_USER_LAST_ACTION), LastAction);
editor.commit();
}
/*
* Saves the given username and userpassword sharedPreferences
* #param String username
* #param String password
*/
private void setUsernameAndPassword(String username, String password) {
editor = sharedPref.edit();
editor.putString(getString(R.string.SETTINGS_USER_USERNAME), username);
editor.putString(getString(R.string.SETTINGS_USER_PASSWORD), password);
editor.commit();
WebServiceUsername = username;
WebServicePassword = password;
}
/*
* Method called when pressing the OK-Button
*/
public void ClickBtnOK(View view) {
// Save User-Creentials
EditText dfsUsername = (EditText) findViewById(R.id.dfsUsername);
String lvsUsername = dfsUsername.getText().toString();
EditText dfsPassword = (EditText) findViewById(R.id.dfsPassword);
String lvsPassword = dfsPassword.getText().toString();
if(lvsUsername.equals("") || lvsPassword.equals("")) {
TextView txtError = (TextView) findViewById(R.id.txtError);
txtError.setText(getString(R.string.ERR_Name_or_Password_empty));
}
else
{
// Save credentials
setUsernameAndPassword(lvsUsername, lvsPassword);
setLastAction(new Date().getTime());
// open Searchactivity
Intent intent = new Intent(this, SearchActivity.class);
startActivity(intent);
}
}
The "log in mask" is setContentView(R.layout.activity_appfragmentactivity);.
The two other activites I created are then extending this parent class. This is one of it:
public class SearchActivity extends AppFragmentActivity {
SearchFragment searchfragment;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
}
#Override
protected void onResume() {
super.onResume();
if(WebServiceUsername.equals("") && WebServicePassword.equals("")) {
// Username not set. Re"login".
Intent intent = new Intent(this, AppFragmentActivity.class);
startActivity(intent);
}
}
#Override
protected void onStart() {
super.onStart();
}
// ...
}
As far as I understand the lifecycle now this should work as the following: When my app starts (the SearchActivity is set for LAUNCH) the app should step into the onResume() of my parent class. There it sees that the credentials are not yet stored and opens the layout of the AppFragmentActivity which is the login. When entered, the user is redirected to the SearchActivity which now sees "ok credentials are there, lets move forward". But this doesnt't happen as the login is not shown up. So I think my onResume() might be wrong. Perhaps my full idea is bad? Up to here I thought I also understand the life cycle, but obviosly I don't?
I then had a look around on SO for similar problems. One thing I saw here was a comment to an user which wanted to build a similar "logout" mechanism as mine, that he has to implement this in every activity. I thought about that and ask myself "Why do I have to override the onResume() in every of my activites, when they are all from the same parent? When theres no onResume() in the child, the one of the parent should be called". The user in the SO-question was advised to use services as background threads to count down a timer in there for the logout. I then read the services article in the docu and then fully got disoriented:
There are two types of services: Started and bounded ones. A started service is once started by an activity and then runs in the background until hell freezes when it doesn't get stoped. So it's fully independed of any app, but the programmer has to / should stop it when it's not longer needed. A bounded services is bounded to one or many app components and stops when all bounded components end. I thought this might be a good alternative for me, but when I thought further I ask myself how: If one of my starts it (let's say the login dialog) and then is closed the service is stoped and the other activites always start there own ones which can't be the sense of it. So this service must be bounded not to any component but to my app. But whats the life cycle of an android app? How can I keep information "global" inside my app. I know I can switch data between actitivites using 'Intents'.
This more and more "foggy cloud" lead to ask myself: "Shall I use only one activity and try to switch in/out everything using fragments?"
So my questions are (I think that's all of them, but I'm not sure anymore):
Does my idea of writing an parent class which does the checks for all extended childs ok or bad AND does it work as I understood it?
Do I have to override every onResume() in the childs just to call the parent one for the checks?
Can you give me a tip why my "login systems" doesn't work?
What's the life cycle of an android app and how can I interact with it?
Shall I only use one activity and switch in/out everything using fragments or is it a good way to have several activities and some of them use fragments (to reuse often used parts)?
Thanks in advise
What I've done in the end is the following:
I removed the "login" thing from the parent class into a stand alone activity. This activity is called when the credentials are not valid together with an finish() of the calling one. So I don't build a loop and drop unused activites.