Skip to content

Instantly share code, notes, and snippets.

@ttaylor
Last active January 5, 2016 20:15
Show Gist options
  • Save ttaylor/11163906 to your computer and use it in GitHub Desktop.
Save ttaylor/11163906 to your computer and use it in GitHub Desktop.
Working Ti.Storekit example for 3.2.2GA. Has now been approved by Apple. This is for non-consummable on-device IAPs only.
var Storekit = require('ti.storekit'),
DZDynamicType = require('com.dezinezync.dynamictype'),
lang = Ti.App.Properties.getString('language');
Storekit.receiptVerificationSandbox = (Ti.App.deployType !== 'production');
Storekit.bundleVersion = 2.3.1; //TODO set current bundle version here
Storekit.bundleIdentifier = bundle; //TODO set bundle id here
var verifyingReceipts = true;
var win = Ti.UI.createWindow({
backgroundColor:'#fff'
});
/*
We want to show the user when we're communicating with the server, so let's define two simple
functions that interact with an activity indicator.
*/
var loading = Ti.UI.createActivityIndicator({
height:50, width:50,
backgroundColor:'black', borderRadius:10,
style:Ti.UI.iPhone.ActivityIndicatorStyle.BIG
});
var loadingCount = 0;
function showLoading()
{
loadingCount += 1;
if (loadingCount == 1) {
loading.show();
}
}
function hideLoading()
{
if (loadingCount > 0) {
loadingCount -= 1;
if (loadingCount == 0) {
loading.hide();
}
}
}
/*
Now let's define a couple utility functions. We'll use these throughout the app.
*/
var tempPurchasedStore = {};
/**
* Tells us if the version of iOS we are running on is iOS 7 or later
*/
function isIOS7Plus()
{
if (Titanium.Platform.name == 'iPhone OS')
{
var version = Titanium.Platform.version.split(".");
var major = parseInt(version[0],10);
// can only test this support on a 3.2+ device
if (major >= 7)
{
return true;
}
}
return false;
}
var IOS7 = isIOS7Plus();
/**
* Keeps track (internally) of purchased products.
* @param identifier The identifier of the Ti.Storekit.Product that was purchased.
*/
function markProductAsPurchased(identifier)
{
Ti.API.info('Marking as purchased: ' + identifier);
// Store it in an object for immediate retrieval.
tempPurchasedStore[identifier] = true;
// And in to Ti.App.Properties for persistent storage.
Ti.App.Properties.setBool('Purchased-' + identifier, true);
}
/**
* Checks if a product has been purchased in the past, based on our internal memory.
* @param identifier The identifier of the Ti.Storekit.Product that was purchased.
*/
function checkIfProductPurchased(identifier)
{
Ti.API.info('Checking if purchased: ' + identifier);
if (tempPurchasedStore[identifier] === undefined)
tempPurchasedStore[identifier] = Ti.App.Properties.getBool('Purchased-' + identifier, false);
return tempPurchasedStore[identifier];
}
/**
* Requests a product. Use this to get the information you have set up in iTunesConnect, like the localized name and
* price for the current user.
* @param identifier The identifier of the product, as specified in iTunesConnect.
* @param success A callback function.
* @return A Ti.Storekit.Product.
*/
function requestProduct(identifier, success)
{
//showLoading();
Storekit.requestProducts([identifier], function (evt) {
//hideLoading();
if (!evt.success) {
alert('ERROR: We failed to talk to Apple!');
}
else if (evt.invalid) {
alert('ERROR: We requested an invalid product!');
}
else {
success(evt.products[0]);
}
});
}
/**
* Purchases a product.
* @param product A Ti.Storekit.Product (hint: use Storekit.requestProducts to get one of these!).
*/
Storekit.addEventListener('transactionState', function (evt) {
//hideLoading();
switch (evt.state) {
case Storekit.TRANSACTION_STATE_FAILED:
if (evt.cancelled) {
alert('Purchase cancelled');
} else {
alert('ERROR: Buying failed! ' + evt.message);
}
evt.transaction && evt.transaction.finish();
break;
case Storekit.TRANSACTION_STATE_PURCHASED:
if (verifyingReceipts) {
if (IOS7) {
// iOS 7 Plus receipt validation is just as secure as pre iOS 7 receipt verification, but is done entirely on the device.
var msg = Storekit.validateReceipt() ? 'Receipt is Valid! Thanks for purchasing!' : 'Receipt is Invalid.';
alert(msg);
if (msg == 'Receipt is Valid! Thanks for purchasing!'){
markProductAsPurchased(evt.productIdentifier);
}
} else {
// Pre iOS 7 receipt verification
Storekit.verifyReceipt(evt, function (e) {
if (e.success) {
if (e.valid) {
alert('Thanks! Receipt Verified');
markProductAsPurchased(evt.productIdentifier);
} else {
alert('Sorry. Receipt is invalid');
}
} else {
alert(e.message);
}
});
}
} else {
alert('Thanks for purchasing!');
markProductAsPurchased(evt.productIdentifier);
}
// If the transaction has hosted content, the downloads property will exist
// Downloads that exist in a PURCHASED state should be downloaded immediately, because they were just purchased.
if (evt.downloads) {
Storekit.startDownloads({
downloads: evt.downloads
});
} else {
// Do not finish the transaction here if you wish to start the download associated with it.
// The transaction should be finished when the download is complete.
// Finishing a transaction before the download is finished will cancel the download.
evt.transaction && evt.transaction.finish();
}
break;
case Storekit.TRANSACTION_STATE_PURCHASING:
Ti.API.info('Purchasing ' + evt.productIdentifier);
break;
case Storekit.TRANSACTION_STATE_RESTORED:
// The complete list of restored products is sent with the `restoredCompletedTransactions` event
Ti.API.info('Restored ' + evt.productIdentifier);
// Downloads that exist in a RESTORED state should not necessarily be downloaded immediately. Leave it up to the user.
if (evt.downloads) {
Ti.API.info('Downloads available for restored product');
}
evt.transaction && evt.transaction.finish();
break;
}
});
function purchaseProduct(product)
{
if (product.downloadable) {
Ti.API.info('Purchasing a product that is downloadable');
}
//showLoading();
Storekit.purchase({
product: product
// applicationUsername is a opaque identifier for the user’s account on your system.
// Used by Apple to detect irregular activity. Should hash the username before setting.
// applicationUsername: '<HASHED APPLICATION USERNAME>'
});
}
/**
* Restores any purchases that the current user has made in the past, but we have lost memory of.
*/
function restorePurchases()
{
//showLoading();
Storekit.restoreCompletedTransactions();
}
Storekit.addEventListener('restoredCompletedTransactions', function (evt) {
//hideLoading();
if (evt.error) {
alert(evt.error);
}
else if (evt.transactions == null || evt.transactions.length == 0) {
alert('There were no purchases to restore!');
}
else {
if (IOS7 && verifyingReceipts) {
if (Storekit.validateReceipt()) {
Ti.API.info('Restored Receipt is Valid!');
} else {
Ti.API.error('Restored Receipt is Invalid.');
}
}
for (var i = 0; i < evt.transactions.length; i++) {
if (!IOS7 && verifyingReceipts) {
Storekit.verifyReceipt(evt.transactions[i], function (e) {
if (e.valid) {
markProductAsPurchased(e.productIdentifier);
} else {
Ti.API.error("Restored transaction is not valid");
}
});
} else {
markProductAsPurchased(evt.transactions[i].productIdentifier);
}
}
alert('Restored ' + evt.transactions.length + ' purchases!');
}
});
/**
* WARNING
* addTransactionObserver must be called after adding the Storekit event listeners.
* Failure to call addTransactionObserver will result in no Storekit events getting fired.
* Calling addTransactionObserver before event listeners are added can result in lost events.
*/
Storekit.addTransactionObserver();
/**
* Validating receipt at startup
* Useful for volume purchase programs.
*/
if (IOS7) {
win.addEventListener('open', function() {
function validate() {
Ti.API.info('Validating receipt.');
Ti.API.info('Receipt is Valid: ' + Storekit.validateReceipt());
}
/*
During development it is possible that the receipt does not exist.
This can be resolved by refreshing the receipt.
*/
if (!Storekit.receiptExists) {
Ti.API.info('Receipt does not exist yet. Refreshing to get one.');
Storekit.refreshReceipt(null, function(){
validate();
});
} else {
Ti.API.info('Receipt does exist.');
validate();
}
});
}
exports.showStore = function(){
win = Ti.UI.createWindow({
title:L(lang+'_store'),
titleAttributes:({
color:'#fff',
font:{fontFamily:'HelveticaNeue-Light', fontSize: Ti.Platform.osname == 'ipad' ? 28 : 24}
}),
backgroundColor:'#fff',
barColor:"#0f75bd",
statusBarStyle: Titanium.UI.iPhone.StatusBar.LIGHT_CONTENT,
width:Ti.UI.FILL,
height:Ti.UI.FILL
});
var tableView = Ti.UI.createTableView({
top: Ti.Platform.osname == 'ipad' ? 20 : 20,
rowHeight:35,
backgroundColor:'transparent',
separatorStyle:Ti.UI.iPhone.TableViewSeparatorStyle.NONE,
showVerticalScrollIndicator:false,
scrollable:Ti.Platform.osname == 'ipad' ? false : true,
});
var closeBtn = Ti.UI.createLabel({
text:'Close',
color:'#fff',
font:{fontFamily:'HelveticaNeue-Light', fontSize:Ti.Platform.osname == 'ipad' ? 24 : 20}
});
closeBtn.addEventListener('click',function(e)
{
navControl.close();
});
win.setLeftNavButton(closeBtn);
var restoreCompletedTransactions = Ti.UI.createLabel({
text:'Restore',
color:'#fff',
font:{fontFamily:'HelveticaNeue-Light', fontSize:Ti.Platform.osname == 'ipad' ? 24 : 20}
});
win.setRightNavButton(restoreCompletedTransactions);
restoreCompletedTransactions.addEventListener('click', function () {
restorePurchases();
});
function addLabel(array,idx,text){
requestProduct(text, function (product) {
var itemTitle = Ti.UI.createLabel({
text:product.title,
color:'#444444',
font:{fontFamily:'HelveticaNeue-Light', fontSize: Ti.Platform.osname == 'ipad' ? DZDynamicType.preferredSize()+8 : DZDynamicType.preferredSize()},
textAlign:Ti.UI.TEXT_ALIGNMENT_LEFT,
height:Ti.UI.SIZE, width:Ti.UI.SIZE,
top:Ti.Platform.osname == 'ipad' ? 15 : 10, left:Ti.Platform.osname == 'ipad' ? 30 : 20, bottom:Ti.Platform.osname == 'ipad' ? 15 : 10
});
var buyItem = Ti.UI.createLabel({
text:'Buy for ' + product.formattedPrice,
color:'#444444',
font:{fontFamily:'HelveticaNeue-Light', fontSize: Ti.Platform.osname == 'ipad' ? DZDynamicType.preferredSize()+8 : DZDynamicType.preferredSize()},
textAlign:Ti.UI.TEXT_ALIGNMENT_LEFT,
height:Ti.UI.SIZE, width:Ti.UI.SIZE,
top:Ti.Platform.osname == 'ipad' ? 15 : 10,right:Ti.Platform.osname == 'ipad' ? 30 : 20,bottom:Ti.Platform.osname == 'ipad' ? 15 : 10
});
buyItem.addEventListener('click', function () {
purchaseProduct(product);
});
array[idx].add(itemTitle);
array[idx].add(buyItem);
});
}
var data = [];
var productTitles = [
//Enter in your product strings here, my app has 13
];
win.add(loading);
showLoading();
//The following appears to be needed as more than seven products cannot be called at the same time.
for (i = 0; num = 7, i < num; i++){
data[i] = Ti.UI.createTableViewRow({selectedBackgroundColor:'transparent', height:Ti.UI.SIZE});
addLabel(data,i,productTitles[i]);
}
setTimeout(function(){
for (i = 6; num = 13, i < num; i++){
data[i] = Ti.UI.createTableViewRow({selectedBackgroundColor:'transparent', height:Ti.UI.SIZE});
addLabel(data,i,productTitles[i]);
}
setTimeout(function(){
tableView.data = data;
win.add(tableView);
hideLoading();
}, 2000);
}, 3000);
var navControl = Ti.UI.iOS.createNavigationWindow({
window : win,
statusBarStyle: Titanium.UI.iPhone.StatusBar.LIGHT_CONTENT
});
return navControl;
};
@mattapperson
Copy link

What was the exact rejection reason?

@ttaylor
Copy link
Author

ttaylor commented Apr 22, 2014

"We found that your app has incorrectly implemented receipt validation, so it does not properly distinguish between the review (sandbox) environment and the production environment. This is resulting in a validation query to the production servers instead of a sandbox receipt check, and is preventing us from being able to review your app."

So I think the problem is line 5, but I'm not sure what has changed from six months ago when the original module example was last updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment