-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Apple In-app purchase (IAP) automatic server-to-server renewal
This commit fixes the issue where some users are not able to get their Purple membership renewed when they are on TestFlight. It does so by implementing a lazy checker that tries to refresh a user's IAP transaction history with Apple's server. The automatic lazy refresh occurs when anyone fetches information about the respective account and: - The account's last transaction is an Apple In-app purchase - The account is not active - IAP history for this account has not been checked for more than 24 hours (to avoid spamming Apple's servers) It also adds automated test coverage around this new functionality. ------ Testing ------ - Passing all pre-existing automated tests (.env independent) - Passing the new automated tests - Performed type checks and found no type-check regressions - Passed manual test below ---------- Manual test ---------- PASS Setup: - Purple server running locally - Purple server with environment correctly setup and connected to IAP sandbox environment - iPhone connected to the local purple server (In developer settings) - iPhone running local build built under the "Release" scheme to connect to IAP sandbox environment - Sandbox App Store account setup and purchase history cleared - Local Purple database cleared - Renewal period set to 5 minutes in Sandbox setting - Second simulator device running on a separate account and also connected to the same local purple server iOS: 17.5 Device: iPhone 13 mini (real device) damus-api: This commit Damus: 1.10 fd130b78e748c7040989f40969c99017256c9418 Steps: 1. Buy Purple through the Sandbox 2. Check account status is as expected 3. Run the database dump script 4. Check the dumped data. There should be one IAP transaction on the database. 5. Check the expiry date (should be 5 minutes from now) 6. Completely close the Damus app 6. Wait for that expiry date 7. Use damus through a second device 8. Visit the profile through this second device. 9. Account should appear with the damus star 10. Dump the local database again 11. The second transaction should have appeared on the tx history, even with no involvement from the primary device 12. Check the new expiry date. 13. Cancel subscription from the primary device 14. Close Damus on all devices 15. Wait for new expiry date 16. Access that profile again from the second device 17. The Damus star should no longer be present. Changelog-Added: Apple In-app purchase automatic server-to-server renewal Closes: #6 Signed-off-by: Daniel D’Aquino <[email protected]> Reviewed-by: William Casarin <[email protected]>
- Loading branch information
1 parent
9b1b63d
commit 2620ad8
Showing
5 changed files
with
169 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
const { get_account_and_user_id, get_user_uuid, add_successful_transactions_to_account, mark_iap_history_was_refreshed } = require('./user_management'); | ||
const { verify_transaction_id } = require('./app_store_receipt_verifier'); | ||
const { current_time } = require('./utils'); | ||
|
||
const IAP_REFRESH_PERIOD = 60 * 60 * 24; // 24 hours | ||
|
||
/** | ||
* Checks an account to see if it needs an update with Apple's IAP servers, and updates it if needed. | ||
* | ||
* @param {Object} app - The app object | ||
* @param {string} pubkey - The public key of the user, hex encoded | ||
* @returns {Promise<{account?: Object, user_id?: number, request_error?: string | null}>} - The account object, the user ID, and the request error, if any | ||
*/ | ||
async function update_iap_history_with_apple_if_needed_and_return_updated_user(app, pubkey) { | ||
let { account, user_id } = get_account_and_user_id(app, pubkey); | ||
if (!account) { | ||
// Account not found | ||
return { account: null, user_id: null, request_error: null }; | ||
} | ||
|
||
if (!should_iap_transaction_history_be_refreshed(account)) { | ||
// No need to refresh iap history | ||
return { account: account, user_id: user_id, request_error: null }; | ||
} | ||
|
||
// Refresh the iap history | ||
const account_uuid = get_user_uuid(app, account.pubkey); | ||
const last_transaction = account.transactions[account.transactions.length - 1]; | ||
try { | ||
let verified_transaction_history = await verify_transaction_id(last_transaction.id, account_uuid); | ||
mark_iap_history_was_refreshed(app, account.pubkey); | ||
if (!verified_transaction_history) { | ||
return { account: account, user_id: user_id }; | ||
} | ||
|
||
const { account: new_account, user_id: latest_user_id, request_error } = add_successful_transactions_to_account(app, account.pubkey, verified_transaction_history); | ||
if (request_error) { | ||
return { account: account, user_id: user_id, request_error: request_error }; | ||
} | ||
return { account: new_account, user_id: latest_user_id } | ||
} catch (error) { | ||
return { account: account, user_id: user_id, request_error: error.message }; | ||
} | ||
} | ||
|
||
async function should_iap_transaction_history_be_refreshed(account) { | ||
const account_active = (account.expiry && current_time() < account.expiry) ? true : false; | ||
const last_transaction = account.transactions[account.transactions.length - 1]; | ||
if (account_active || last_transaction == undefined || last_transaction.type != "iap") { | ||
// No need to update iap history because account is either active, or the last transaction was not an IAP transaction | ||
return false; | ||
} | ||
|
||
if (account.last_iap_history_refresh && (current_time() - account.last_iap_history_refresh) < IAP_REFRESH_PERIOD) { | ||
// We already checked with Apple in the last 24 hours. No need to check again for now. | ||
return false; | ||
} | ||
|
||
// If the account is inactive and the last transaction was an IAP, we should check with Apple with it was renewed. | ||
return true; | ||
} | ||
|
||
module.exports = { update_iap_history_with_apple_if_needed_and_return_updated_user, should_iap_transaction_history_be_refreshed }; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters