diff --git a/README.md b/README.md index 6660a5f..07e51c9 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,20 @@ iap.cancelSubscription("google", payment, function (error, response) { }); ``` +### Subscription deferral ( Google Play only ) + +Google exposes [an API for deferral](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/defer) of recurring suscriptions. This might be used to extend a user's subscription purchase until a specified future expiration time ( useful to grant your users some free days or months ). + +```javascript +var deferralInfo = { + expectedExpiryTimeMillis: 1546616722237, + desiredExpiryTimeMillis: 1547716722237, +}; +iap.deferSubscription("google", payment, deferralInfo, function (error, response) { + /* your code */ +}); +``` + ## Supported platforms ### Amazon diff --git a/index.js b/index.js index e7220b4..af022ad 100644 --- a/index.js +++ b/index.js @@ -67,3 +67,39 @@ exports.cancelSubscription = function (platform, payment, cb) { cb(null, result); }); }; + + +exports.deferSubscription = function (platform, payment, deferralInfo, cb) { + function syncError(error) { + process.nextTick(function () { + cb(error); + }); + } + + if (!payment) { + return syncError(new Error('No payment given')); + } + + if (!deferralInfo) { + return syncError(new Error('No deferralInfo given')); + } + + const engine = platforms[platform]; + + if (!engine) { + return syncError(new Error(`Platform ${platform} not recognized`)); + } + + if (!engine.deferSubscription) { + return syncError(new Error(`Platform ${platform + } does not have deferSubscription method`)); + } + + engine.deferSubscription(payment, deferralInfo, function (error, result) { + if (error) { + return cb(error); + } + + cb(null, result); + }); +}; diff --git a/lib/google/index.js b/lib/google/index.js index ca4ae9e..9bc27cd 100644 --- a/lib/google/index.js +++ b/lib/google/index.js @@ -41,6 +41,17 @@ function validatePaymentAndParseKeyObject(payment) { return keyObject; } +function validateDeferralInfo(deferralInfo) { + assert.equal(typeof deferralInfo, 'object', 'deferralInfo must be an object'); + assert.equal(typeof deferralInfo.expectedExpiryTimeMillis, 'number', 'expectedExpiryTimeMillis must be a number'); + assert.equal(typeof deferralInfo.desiredExpiryTimeMillis, 'number', 'desiredExpiryTimeMillis must be a number'); + + assert(deferralInfo.desiredExpiryTimeMillis > deferralInfo.expectedExpiryTimeMillis, 'desiredExpiryTimeMillis must be greater than expectedExpiryTimeMillis'); + + return deferralInfo; +} + + exports.verifyPayment = function (payment, cb) { let keyObject; @@ -133,3 +144,52 @@ exports.cancelSubscription = function (payment, cb) { }); }); }; + + +exports.deferSubscription = function (payment, deferralInfo, cb) { + let keyObject; + let options; + + try { + keyObject = validatePaymentAndParseKeyObject(payment); + options.json = { + deferralInfo: validateDeferralInfo(deferralInfo) + }; + } catch (error) { + return process.nextTick(function () { + cb(error); + }); + } + + jwt.getToken(keyObject.client_email, keyObject.private_key, apiUrls.publisherScope, function (error, token) { + if (error) { + return cb(error); + } + + const requestUrl = apiUrls.purchasesSubscriptionsDefer( + payment.packageName, + payment.productId, + payment.receipt, + token + ); + + https.post(requestUrl, options, function (error, res, resultString) { + if (error) { + return cb(error); + } + + if (res.statusCode !== 200) { + return cb(new Error(`Received ${res.statusCode} status code with body: ${resultString}`)); + } + + var resultObject; + try { + resultObject = JSON.parse(resultString); + } catch (e) { + return cb(e); + } + + return cb(null, resultObject); + }); + }); +}; diff --git a/lib/google/urls.js b/lib/google/urls.js index ed23521..9ca002e 100644 --- a/lib/google/urls.js +++ b/lib/google/urls.js @@ -38,8 +38,6 @@ exports.purchasesSubscriptionsGet = function (packageName, productId, receipt, a ); }; - -// Android Subscriptions URLs & generators exports.purchasesSubscriptionsCancel = function (packageName, productId, receipt, accessToken) { const urlFormat = 'https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s:cancel?access_token=%s'; @@ -51,3 +49,16 @@ exports.purchasesSubscriptionsCancel = function (packageName, productId, receipt encodeURIComponent(accessToken) // API access token ); }; + +// Android Subscriptions URLs & generators +exports.purchasesSubscriptionsDefer = function (packageName, productId, receipt, accessToken) { + const urlFormat = 'https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s:defer?access_token=%s'; + + return util.format( + urlFormat, + encodeURIComponent(packageName), // application package name + encodeURIComponent(productId), // productId + encodeURIComponent(receipt), // purchase token + encodeURIComponent(accessToken) // API access token + ); +}; diff --git a/package.json b/package.json index d97eff6..d770d32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "iap", - "version": "1.1.0", + "name": "@emma-app/iap", + "version": "1.1.1", "description": "In-app purchase validation for Apple, Google, Amazon, Roku", "main": "index.js", "scripts": { @@ -8,7 +8,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/Wizcorp/node-iap" + "url": "https://github.com/emma-app/node-iap" }, "keywords": [ "iap", @@ -21,7 +21,16 @@ "purchase", "itunes" ], - "author": "Ron Korving ", + "author": { + "name": "Ron Korving", + "email": "rkorving@wizcorp.jp" + }, + "contributors": [ + { + "name": "Varadh Kalidasan", + "email": "varadh@emma-app.com" + } + ], "license": "MIT", "dependencies": { "jwt-simple": "0.5.6",