Can't finish transaction in ionic-native/in-app-purchase-2 - javascript

I'm developing app with Ionic 4 / Angular 8 / Cordova
I have installed In App Purchase 2 and setup it. Apple Developer and Sandbox accounts are ok.
Product registration is ok:
registerProduct() {
this.iap.verbosity = this.iap.DEBUG;
this.iap.register({
id: MONEYCOMBO_KEY,
type: this.iap.CONSUMABLE
})
this.registerHandlersForPurchase(MONEYCOMBO_KEY)
this.product = this.iap.get(MONEYCOMBO_KEY)
this.iap.refresh()
this.iap.when(MONEYCOMBO_KEY).updated((p) => {
this.product = p
this.title = p.title
this.price = p.price
})
}
Event handlers:
registerHandlersForPurchase(productId) {
let self = this.iap;
this.iap.when(productId).updated(function (product) {
if (product.loaded && product.valid && product.state === self.APPROVED && product.transaction != null) {
product.finish();
}
});
this.iap.when(productId).registered((product: IAPProduct) => {
// alert(` owned ${product.owned}`);
});
this.iap.when(productId).owned((product: IAPProduct) => {
console.error('finished')
product.finish();
});
this.iap.when(productId).approved((product: IAPProduct) => {
// alert('approved');
product.finish();
});
this.iap.when(productId).refunded((product: IAPProduct) => {
// alert('refunded');
});
this.iap.when(productId).expired((product: IAPProduct) => {
// alert('expired');
});
}
Purchase method:
buyMoneyCombo(form: NgForm) {
this.registerHandlersForPurchase(MONEYCOMBO_KEY)
this.date = form.value.date
this.iap.order(MONEYCOMBO_KEY)
this.iap.refresh()
}
The problem:
Console says:
"InAppPurchase[js]: product test has a transaction in progress: 1000000628239595"
Transaction cannot be finished. Why?

So, after a weeks researching, I found the solution.
And there is no problem:
Message from Apple is displaying after transaction is finished:
"InAppPurchase[js]: product test has a transaction in progress: 1000000628239595"
And all purchase logic must be declared inside:
this.iap.when(MONEYCOMBO_KEY).approved((product: IAPProduct) => {
product.finish()
this.getMoney()
})

Related

Javascript constructor calls

Hi so in the app I am working on, I have this constructor that checks the redux store for error messages being passed by various components. It displays the error or success messages just fine. However once the user dismisses the banner (by clicking x) and I go to another person's portfolio the banner no longer shows error or success messages
constructor(private store: Store<State>) {
store
.select(StatusBannerState)
.map(data => {
return data.status_banner;
})
.subscribe(banner_state => {
if (banner_state.success_list.length > 0) {
this.showBanner = true;
this.bannerMessage = this.createSuccessBannerMessage(
banner_state.success_list
);
setTimeout(() => {
this.store.dispatch(new BannerDimissesSuccessMessage());
this.bannerMessage = this.createErrorBannerMessage(
banner_state.error_list
);
}, 5000);
} else if (banner_state.error_list.length > 0) {
this.bannerMessage = this.createErrorBannerMessage(
banner_state.error_list
);
} else {
this.showBanner = false;
this.bannerMessage = '';
}
});
}
I have this test function at the moment which I call in the createErrorMessage function to show or hide the funciton (I call it in the HTML component of the angular app)
showOrHideBanner(errorWidget) {
errorWidget.length === 0
? (this.showBanner = false)
: (this.showBanner = true);
}
I have another method that clears the redux store on initialization
ngOnInit() {
this.store.dispatch(new UserDismissesEverything());
}
What would be the best way to check for error messages again after the user has dismissed the banner
update: code for close
onCloseClick() {
this.close.emit(true);
this.show = false;
this.clearTimeout(this.timeoutId);
}
HTML component code
<div class="mt-1 container">
<div class="alert-box">
<div *ngIf="bannerMessage" class="px-3 mb-1">
<glass-alert-box
(close)="hideTheBanner()"
[success]="bannerMessageType == 'success'">{{ bannerMessage}}
</glass-alert-box>
</div>
</div>
</div>
Try following code:
constructor(private store: Store<State>) {
}
ngOnInint() {
this.store.dispatch(new UserDismissesEverything());
}
ngAfterViewInint() {
this.store.select(StatusBannerState).map(data => {
return data.status_banner;
}).subscribe(banner_state => {
if (banner_state.success_list.length > 0) {
this.showBanner = true;
this.bannerMessage = this.createSuccessBannerMessage(banner_state.success_list);
setTimeout(() => {
this.store.dispatch(new BannerDimissesSuccessMessage());
this.bannerMessage = this.createErrorBannerMessage(banner_state.error_list);
}, 5000);
} else if (banner_state.error_list.length > 0) {
this.bannerMessage = this.createErrorBannerMessage(banner_state.error_list);
} else {
this.showBanner = false;
this.bannerMessage = '';
}
});
}

Firebase charge function not being received by Stripe

I'm working with charging credit cards on an ionic application. If I check the Stripe website, only the token is created successfully. There is no event for the charge. When I check Firebase everything seems to be working (previously I had error message "An error occurred with our connection to Stripe" which was eliminated by upgrading to paid version Blaze).
I've structured the following code so you can see the sequence of events.
Here is the sequence of events:
1) Here I get the token and pass to payByStripe function:
checkOut() {
let alert = this.alertCtrl.create({
title: 'card information',
cssClass:'alert-css',
inputs: [{
name: 'cardNumber',
placeholder: 'card number',
},
{
name: 'expMonth',
placeholder: 'expMonth',
},
{
name: 'expYear',
placeholder: 'expYear',
},
{
name: 'cvc',
placeholder: 'cvc',
}],
buttons: [{
text: 'Cancel',
role: 'cancel',
handler: data => {
console.log('Cancel clicked');
}
},
{
text: 'PAY',
handler: data => {
this.stripe.setPublishableKey('pk_test_abcdef1BC');
let card = {
number: data.cardNumber,
expMonth: data.expMonth,
expYear: data.expYear,
cvc: data.cvc
};
this.stripe.createCardToken(card)
.then(token => {
this.goodsData.payByStripe(this.totalMoniesPlusTaxAndCharge,this.userEmail,token);
let alert = this.alertCtrl.create({
title: 'Charged',
cssClass:'alert-css',
subTitle: 'Successful Charge!',
buttons: ['Dismiss']
});
alert.present();
}, error => {
this.loading.dismiss();
let alert = this.alertCtrl.create({
title: 'ERROR',
cssClass:'alert-css',
subTitle: JSON.stringify(error),
buttons: ['Dismiss']
});
alert.present();
})
.catch(error => console.log(JSON.stringify(error)) );
}
}]
});
alert.present();
}
2) Here, I push the charge to Firebase, where cloud functions will handle it:
payByStripe(amount,email,token): firebase.database.Reference {
return firebase.database().ref('/stripe_customers/charges/').push({
amount:amount,
email:email,
token:token
});
}
3) Here is what the cloud functions are doing:
const functions = require('firebase-functions'),
admin = require('firebase-admin'),
logging = require('#google-cloud/logging')();
admin.initializeApp(functions.config().firebase);
const stripe = require('stripe')(functions.config().stripe.token),
currency = functions.config().stripe.currency || 'USD';
// [START chargecustomer]
// Charge the Stripe customer whenever an amount is written to the Realtime database
exports.createStripeCharge = functions.database.ref('/stripe_customers/charges/').onWrite(event => {
const val = event.data.val();
// This onWrite will trigger whenever anything is written to the path, so
// noop if the charge was deleted, errored out, or the Stripe API returned a result (id exists)
if (val === null || val.id || val.error) return null;
// Look up the Stripe customer id written in createStripeCustomer
// Create a charge using the pushId as the idempotency key, protecting against double charges
const amount = val.amount;
const email = val.email;
const token = val.token;
stripe.charges.create({
amount: amount,
currency: "usd",
source: token,
description: "Charge for " + email
}).then(response => {
// If the result is successful, write it back to the database
return event.data.adminRef.set(response);
}, error => {
// We want to capture errors and render them in a user-friendly way, while
// still logging an exception with Stackdriver
return event.data.adminRef.child('error').set(userFacingMessage(error)).then(() => {
return reportError(error, {user: event.params.userId});
});
});
});
// [END chargecustomer]]
I'm pretty sure that's a Firebase-specific error, so if the cause isn't clear, you may want to reach out to them - though someone else with more Firebase experience may have a more useful answer for you!

Ionic 3 : Close modal with phone's back button

I try to override the phone's back button in my Ionic App.
This code permit me to open a modal to close the App if I'm not in a page, else close the page.
But this doesn't allow me to close an opened modal. How can I detect if I'm in a modal to close it ?
platform.registerBackButtonAction(() => {
let nav = app.getActiveNav();
let activeView: ViewController = nav.getActive();
console.log(activeView);
if(activeView != null){
if(nav.canGoBack()) {
activeView.dismiss();
} else{
let alert = this.alertCtrl.create({
title: this.pdataManager.translate.get("close-app"),
message: this.pdataManager.translate.get("sure-want-leave"),
buttons: [
{
text: this.pdataManager.translate.get("no"),
handler: () => {
this.presentedAlert = false;
},
role: 'cancel',
},
{
text: this.pdataManager.translate.get("yes"),
handler: () => {
this.presentedAlert = false;
this.platform.exitApp();
}
}
]
});
if(!this.presentedAlert) {
alert.present();
this.presentedAlert = true;
}
}
}
});
}
1.Import IonicApp:
import {IonicApp } from 'ionic-angular';
2.Add to your constructor:
private ionicApp: IonicApp
3.Inside your platform.registerBackButtonAction add:
let activeModal=this.ionicApp._modalPortal.getActive();
if(activeModal){
activePortal.dismiss();
return;
}
I found the answer here :
https://github.com/ionic-team/ionic/issues/6982
You can give page name to your modal and you can access it from anywhere in app. Try this..
import { App } from 'ionic-angular';
constructor(public app: App){
}
platform.registerBackButtonAction(() => {
let nav = this.app.getActiveNav();
let view = nav.getActive().instance.pageName;
if (view == YOU_PAGE_NAME) {
//You are in modal
} else {
//You are not in modal
}
});
Inside your modal
pageName = 'YOU_PAGE_NAME';
In the end I have this for my back button:
constructor(private platform: Platform, private config: ConfigService, private nfc: NfcService, private alertCtrl: AlertController,
public events: Events, private translate: TranslateService, private fetch: ConfigFetchService, private menuList: MenuList, private ionicApp: IonicApp,
private menuCtrl: MenuController
) {
platform.ready().then(() => {
this.config.pullVersion();
let ready = true;
platform.registerBackButtonAction(() => {
Logger.log("Back button action called");
let activePortal = ionicApp._loadingPortal.getActive() ||
ionicApp._modalPortal.getActive() ||
ionicApp._toastPortal.getActive() ||
ionicApp._overlayPortal.getActive();
if (activePortal) {
ready = false;
activePortal.dismiss();
activePortal.onDidDismiss(() => { ready = true; });
Logger.log("handled with portal");
return;
}
if (menuCtrl.isOpen()) {
menuCtrl.close();
Logger.log("closing menu");
return;
}
let view = this.nav.getActive();
let page = view ? this.nav.getActive().instance : null;
if (page && page.isRootPage) {
Logger.log("Handling back button on a home page");
this.alertCtrl.create({
title: translate.instant('Confirmation'),
message: translate.instant('Do you want to exit?'),
buttons: [
{
text: translate.instant('Cancel'),
handler: () => {
}
},
{
text: translate.instant('OK'),
handler: () => {
platform.exitApp();
}
}
]
}).present();
}
else if (this.nav.canGoBack() || view && view.isOverlay
) {
Logger.log("popping back");
this.nav.pop();
}
else if (localStorage.getItem('is_logged_in')
) {
Logger.log("Returning to home page");
this.nav.setRoot(HomePage);
}
else if (!localStorage.getItem('is_logged_in')) {
Logger.log("Not yet logged in... exiting");
platform.exitApp();
}
else {
Logger.log("ERROR with back button handling");
}
}, 1);
....

Meteor Blaze order sub-documents by sub-document property

Profile:
_id: Pe0t3K8GG8,
videos: [
{id:'HdaZ8rDAmy', url:'VIDURL', rank: 2},
{id:'22vZ8mj9my', url:'VIDURL2', rank: 0},
{id:'8hyTlk8H^6', url:'VIDURL3', rank: 1},
]
The profile is displayed together with the list of videos. I have a Drag & Drop which updates the videos rank using a Server Method.
1) the database updates correctly on Drop.
2) To sort the videos Array - I declare a helper on the Profile Template and SORT the videos array based on a custom comparison function.
Template.Profile.helpers({
'videosSorted': function(){
let videos = (this.videos);
let videosSorted = videos.sort(function(a, b) {
return parseFloat(a.rank) - parseFloat(b.rank);
});
return videosSorted;
}
});
Problem:
A) In Blaze the {{#each videosSorted}} does not reactively update.
If I F5 refresh then i can see the new order.
I think the issue is because I am providing videosSorted which does not update on changes to the document in the db.
How can I make videosSorted reactive?
Update:
All related code:
Iron Router Controller - I subscribe and set the data context for the layout
ProfileController = RouteController.extend({
subscriptions: function() {
this.subscribe('profile',this.params.slug).wait();
},
data: function () {
//getting the data from the subscribed collection
return Profiles.findOne({'slug':this.params.slug});
},
})
Publication:
Meteor.publish('profile', function (slug) {
const profile = Profiles.find({"slug":slug});
if(profile){
return profile;
}
this.ready();
});
The Profile HTML template:
<template name="Profile">
<ul class="sortlist">
{{#each videosSorted}}
{{> Video}}
{{/each}}
</ul>
</template>
I am using mrt:jquery-ui - sortable function
Template.Profile.onRendered(function () {
thisTemplate = this;
this.$('.sortlist').sortable({
stop: function(e, ui) {
el = ui.item.get(0);
before = ui.item.prev().get(0);
after = ui.item.next().get(0);
if(!before) {
newRank = Blaze.getData(after).rank - 1
} else if(!after) {
newRank = Blaze.getData(before).rank + 1
}
else {
newRank = (Blaze.getData(after).rank +
Blaze.getData(before).rank) / 2
}
let queryData = {
_id: thisTemplate.data._id, //the id of the profile record
videos_objId: Blaze.getData(el).objId, //the id of the sub document to update
new_rank: newRank //the new rank to give it
};
//Update the sub document using a server side call for validation + security
Meteor.call("updateVideoPosition", queryData, function (error, result) {
if(!result){
console.log("Not updated");
}
else{
console.log("successfully updated Individual's Video Position")
}
});
}
})
});
And finally the Meteor method that does the updating
'updateVideoPosition': function (queryData){
let result = Individuals.update(
{_id: queryData._id, 'videos.objId': queryData.videos_objId },
{ $set:{ 'videos.$.rank' : queryData.new_rank } }
)
return result;
}
Note :
As i mentioned - the database updates correctly - and if i have an Incognito window open to the same page - i see the videos reactivly (magically !) switch to the new order.
The schema
const ProfileSchema = new SimpleSchema({
name:{
type: String,
}
videos: {
type: [Object],
optional:true,
},
'videos.$.url':{
type:String,
},
'videos.$.rank':{
type:Number,
decimal:true,
optional:true,
autoform: {
type: "hidden",
}
},
'videos.$.subCollectionName':{
type:String,
optional:true,
autoform: {
type: "hidden",
}
},
'videos.$.objId':{
type:String,
optional:true,
autoform: {
type: "hidden",
}
}
});
I came up with really crude solution, but I don't see other options right now. The simplest solution I can think of is to rerender template manually:
Template.Profile.onRendered(function () {
var self = this;
var renderedListView;
this.autorun(function () {
var data = Template.currentData(); // depend on tmeplate data
//rerender video list manually
if (renderedListView) {
Blaze.remove(renderedListView);
}
if (data) {
renderedListView = Blaze.renderWithData(Template.VideoList, data, self.$('.videos-container')[0]);
}
});
});
Template.VideoList.onRendered(function () {
var tmpl = this;
tmpl.$('.sortlist').sortable({
stop: function (e, ui) {
var el = ui.item.get(0);
var before = ui.item.prev().get(0);
var after = ui.item.next().get(0);
var newRank;
if (!before) {
newRank = Blaze.getData(after).rank - 1
} else if (!after) {
newRank = Blaze.getData(before).rank + 1
}
else {
newRank = (Blaze.getData(after).rank +
Blaze.getData(before).rank) / 2
}
let queryData = {
_id: tmpl.data._id, //the id of the profile record
videos_objId: Blaze.getData(el).objId, //the id of the sub document to update
new_rank: newRank //the new rank to give it
};
//Update the sub document using a server side call for validation + security
Meteor.call("updateVideoPosition", queryData, function (error, result) {
if (!result) {
console.log("Not updated");
}
else {
console.log("successfully updated Individual's Video Position")
}
});
}
});
});
Template.VideoList.helpers({
videosSorted: function () {
return this.videos.sort(function (a, b) {
return a.rank - b.rank;
});
}
});
And HTML:
<template name="Profile">
<div class="videos-container"></div>
</template>
<template name="VideoList">
<ul class="sortlist">
{{#each videosSorted}}
<li>{{url}}</li>
{{/each}}
</ul>
</template>
Reativeness was lost in your case because of JQuery UI Sortable. It doesn't know anything about Meteor's reactiveness and simply blocks template rerendering.
Probably you should consider using something more adopted for Meteor like this (I am not sure it fits your needs).

display only the elements with checked = true

I have a list of items with the option to checked or unchecked.
<ion-item ng-repeat="sport in sports"
ng-click="toggleSportSelection(sport)">
{{:: sport.name}}
</ion-item>
if those items are unchecked you are unable to see them here
<div ng-show="sport.checked" ng-repeat="sport in sports">
{{sport.name}}
</div>
those items has been saved in a DB every time you unchecked them.
The reason why I am here, is because the default behavior of the items is checked = true so it doesn't matter if they are saved in a DB, if you refresh the page, all the items are set up to checked = true again.
So what can I do in order to avoid that behavior and that the app recognizes once the items are unchecked or checked ?
this is part of the controller
.controller('SportsController', function($scope, SportsFactory,
AuthFactory) {
SportsFactory.getSportChecked(customer).then(function(sportChecked) {
_.each(sports, function(sport) {
var intersectedSports = _.intersection(sport.id, sportChecked),
checkedSportObjects = _.filter(sport, function(sportObj) {
return _.includes(intersectedSports, sportObj);
});
_.each(checkedSportObjects, function(sport) {
$scope.sports.push(sport);
});
});
//here is the part where the default behavior is checked = true
if (sports.length) {
$scope.sports = _.map(sports, function(sport) {
sport.checked = true;
return sport;
});
}
$scope.toggleSportSelection = function(sport) {
var params = {};
params.user = $scope.customer.customer;
params.sport = sport.id;
sport.checked = !sport.checked;
SportsFactory.setSportChecked(params);
};
});
UPDATE
service.js
setSportChecked: function(params) {
var defer = $q.defer();
$http.post(CONSTANT_VARS.BACKEND_URL + '/sports/checked', params)
.success(function(sportChecked) {
LocalForageFactory.remove(CONSTANT_VARS.LOCALFORAGE_SPORTS_CHECKED, params);
defer.resolve(sportChecked);
})
.error(function(err) {
console.log(err);
defer.reject(err);
});
return defer.promise;
},
and the NODEJS part
sportChecked: function(params) {
var Promise = require('bluebird');
return new Promise(function(fullfill, reject) {
console.time('sportChecked_findOne');
SportSelection.findOne({
user: params.user
}).exec(function(err, sportChecked) {
console.timeEnd('sportChecked_findOne');
var newSport;
if (err) {
reject(new Error('Error finding user'));
console.error(err);
}else if (sportChecked) {
newSport = sportChecked.sport || [];
console.log(newSport);
console.time('sportChecked_update');
if (_.includes(sportChecked.sport, params.sport)) {
console.log('Sport already exists');
console.log(sportChecked.sport);
sportChecked.sport = _.pull(newSport, params.sport);
// sportChecked.sport = _.difference(newSport, params.sport);
console.log(sportChecked.sport);
}else {
newSport.push(params.sport);
sportChecked.sport = newSport;
}
SportSelection.update({
user: params.user
},
{
sport: newSport
}).exec(function(err, sportCheckedUpdated) {
console.timeEnd('sportChecked_update');
if (err) {
reject(new Error('Error on sportChecked'));
}else {
fullfill(sportCheckedUpdated);
}
});
if (sportChecked.sport) {
sportChecked.sport.push(params.sport);
console.log('New sport added');
}else {
sportChecked.sport = [params.sport];
}
}else {
console.time('sportChecked_create');
SportSelection.create({
sport: [params.sport],
user: params.user
}).exec(function(err, created) {
console.timeEnd('sportChecked_create');
if (err) {
reject(new Error('Error on sportChecked'));
}else {
fullfill(created);
}
});
}
});
});
},
I am using lodash so I will appreciate if you can assist me with that.
My issue itself is: it doesn't matter if the items are unchecked, once you refresh the page, all the items will be set up to checked = true again.
Someone says that I can use _.difference, but how ? or what can I do? I am here to read your suggestions.
Something like this?
.controller('SportsController', function($scope, SportsFactory) {
// get a list of all sports, with default value false
SportsFactory.getAllSports().then(function(sports){
$scope.sports = sports;
// set all items to unchecked
angular.each($scope.sports, function(sport) {
sport.checked = false;
});
// get a list of checked sports for customer
SportsFactory.getCheckedSports(customer).then(function(checkedSports)
{
// set the sports in your list as checked
angular.each(checkedSports, function(checkedSport){
var sport = _.findWhere($scope.sports, {id: checkedSport.id});
sport.checked = true;
});
});
$scope.toggleSportSelection = function(sport) {
// do your toggle magic here
};
});
In your view use a filter:
<div ng-repeat="sport in sports | filter:{checked:true}">
{{sport.name}}
</div>

Categories

Resources