diff --git a/src/components/Settings/Settings.component.js b/src/components/Settings/Settings.component.js index de13fce71..bbfa9ce4c 100644 --- a/src/components/Settings/Settings.component.js +++ b/src/components/Settings/Settings.component.js @@ -25,7 +25,7 @@ import FullScreenDialog from '../UI/FullScreenDialog'; import Paper from '@material-ui/core/Paper'; import UserIcon from '../UI/UserIcon'; import SettingsTour from './SettingsTour.component'; -import { isCordova, isAndroid, isIOS } from '../../cordova-util'; +import { isCordova, isAndroid } from '../../cordova-util'; import './Settings.css'; import { CircularProgress } from '@material-ui/core'; @@ -96,7 +96,7 @@ export class Settings extends PureComponent { } ]; - if (!isIOS() && !isInFreeCountry) { + if (!isInFreeCountry) { const subscribeSection = { icon: , text: messages.subscribe, diff --git a/src/components/Settings/Subscribe/Subscribe.component.js b/src/components/Settings/Subscribe/Subscribe.component.js index 1188b7721..0675b22f0 100644 --- a/src/components/Settings/Subscribe/Subscribe.component.js +++ b/src/components/Settings/Subscribe/Subscribe.component.js @@ -9,6 +9,7 @@ import './Subscribe.css'; import SubscriptionInfo from './SubscriptionInfo'; import SubscriptionPlans from './SubscriptionPlans'; +import { CircularProgress } from '@material-ui/core'; const propTypes = { /** @@ -29,7 +30,8 @@ const propTypes = { onRefreshSubscription: PropTypes.func, onSubscribeCancel: PropTypes.func.isRequired, onCancelSubscription: PropTypes.func.isRequired, - cancelSubscriptionStatus: PropTypes.string.isRequired + cancelSubscriptionStatus: PropTypes.string.isRequired, + updatingStatus: PropTypes.bool.isRequired }; const defaultProps = { @@ -46,7 +48,8 @@ const Subscribe = ({ onSubscribeCancel, onPaypalApprove, onCancelSubscription, - cancelSubscriptionStatus + cancelSubscriptionStatus, + updatingStatus }) => { return (
@@ -56,7 +59,11 @@ const Subscribe = ({ onClose={onClose} // fullWidth > - {!subscription.isSubscribed ? ( + {updatingStatus ? ( +
+ +
+ ) : !subscription.isSubscribed ? ( { return p.id === product.subscriptionId; @@ -207,7 +210,10 @@ export class SubscribeContainer extends PureComponent { try { await window.CdvPurchase.store.update(); offers = prod.offers; - offer = offers.find(offer => offer.tags[0] === product.tag); + const findCallback = isAndroid() + ? offer => offer.tags[0] === product.tag + : offer => offer.productId === product.subscriptionId; + offer = offers.find(findCallback); } catch (err) { console.error('Cannot subscribe product. Error: ', err.message); this.handleError(err); @@ -215,6 +221,11 @@ export class SubscribeContainer extends PureComponent { } } + const filterInAppPurchaseIOSTransactions = uniqueReceipt => + uniqueReceipt.transactions.filter( + transaction => transaction.transactionId !== 'appstore.application' + ); + try { // update the api const requestOrigin = @@ -225,40 +236,77 @@ export class SubscribeContainer extends PureComponent { // check if current subscriber already bought in this device if (localReceipts.length) { const lastReceipt = localReceipts.slice(-1)[0]; - if ( - lastReceipt && - lastReceipt?.transactions[0]?.nativePurchase?.orderId !== - subscriber.transaction?.transactionId - ) { - this.handleError({ - code: '0001', - message: intl.formatMessage(messages.googleAccountAlreadyOwns) - }); - return; + if (isAndroid()) { + if ( + lastReceipt && + lastReceipt?.transactions[0]?.nativePurchase?.orderId !== + subscriber.transaction?.transactionId + ) { + this.handleError({ + code: '0001', + message: intl.formatMessage(messages.googleAccountAlreadyOwns) + }); + return; + } + } + if (isIOS()) { + //IOS have a unique receipt here => 'lastReceipt' + const inAppPurchaseTransactions = filterInAppPurchaseIOSTransactions( + localReceipts[0] + ); + + const lastTransaction = inAppPurchaseTransactions.slice(-1)[0]; + if ( + inAppPurchaseTransactions.length > 0 && + lastTransaction?.transactionId !== + subscriber.transaction?.transactionId + ) { + this.handleError({ + code: '0001', + message: intl.formatMessage(messages.appleAccountAlreadyOwns) + }); + return; + } } } + await API.updateSubscriber(apiProduct); // proceed with the purchase - if (isAndroid()) { + if (isAndroid() || isIOS()) { const order = await window.CdvPurchase.store.order(offer); if (order && order.isError) throw order; updateSubscription({ ownedProduct: { ...product, - platform: 'android-playstore' + platform: isAndroid() ? 'android-playstore' : 'app-store' } }); } } catch (err) { if (err.response?.data.error === 'subscriber not found') { // check if current subscriber already bought in this device - if (localReceipts.length) { - this.handleError({ - code: '0001', - message: intl.formatMessage(messages.googleAccountAlreadyOwns) - }); - return; + if (isAndroid()) { + if (localReceipts.length) { + this.handleError({ + code: '0001', + message: intl.formatMessage(messages.googleAccountAlreadyOwns) + }); + return; + } + } + if (isIOS()) { + const localInAppPurchaseTransactions = filterInAppPurchaseIOSTransactions( + localReceipts[0] + ); + + if (localInAppPurchaseTransactions.length) { + this.handleError({ + code: '0001', + message: intl.formatMessage(messages.appleAccountAlreadyOwns) + }); + return; + } } try { const newSubscriber = { @@ -269,13 +317,13 @@ export class SubscribeContainer extends PureComponent { }; const res = await API.createSubscriber(newSubscriber); updateSubscriberId(res._id); - if (isAndroid()) { + if (isAndroid() || isIOS()) { const order = await window.CdvPurchase.store.order(offer); if (order && order.isError) throw order; updateSubscription({ ownedProduct: { ...product, - platform: 'android-playstore' + platform: isAndroid() ? 'android-playstore' : 'app-store' } }); } @@ -306,6 +354,7 @@ export class SubscribeContainer extends PureComponent { onPaypalApprove={this.handlePaypalApprove} onCancelSubscription={this.handleCancelSubscription} cancelSubscriptionStatus={this.state.cancelSubscriptionStatus} + updatingStatus={this.state.updatingStatus} /> ); } diff --git a/src/components/Settings/Subscribe/Subscribe.css b/src/components/Settings/Subscribe/Subscribe.css index 687df3158..97b778ad0 100644 --- a/src/components/Settings/Subscribe/Subscribe.css +++ b/src/components/Settings/Subscribe/Subscribe.css @@ -1,3 +1,10 @@ +.Subscribe__Loading__Container { + padding: 1em; + height: 92vh; + display: flex; + justify-content: center; + align-items: center; +} .Subscribe__Alert { margin: 8px 0; } diff --git a/src/components/Settings/Subscribe/Subscribe.messages.js b/src/components/Settings/Subscribe/Subscribe.messages.js index 37d4e4c6d..08fa05288 100644 --- a/src/components/Settings/Subscribe/Subscribe.messages.js +++ b/src/components/Settings/Subscribe/Subscribe.messages.js @@ -89,6 +89,11 @@ export default defineMessages({ id: 'cboard.components.Settings.Subscribe.not_subscribed', defaultMessage: 'You are not subscribed. ' }, + unverified: { + id: 'cboard.components.Settings.Subscribe.unverified', + defaultMessage: + 'An error occurred during validation of your purchase. Please refresh' + }, refresh: { id: 'cboard.components.Settings.Subscribe.refresh', defaultMessage: 'Refresh' @@ -148,6 +153,11 @@ export default defineMessages({ defaultMessage: 'It looks like your Google account has already purchased a product. Try restarting the app.' }, + appleAccountAlreadyOwns: { + id: 'cboard.components.Settings.Subscribe.appleAccountAlreadyOwns', + defaultMessage: + 'It looks like your Apple account has already purchased a product. Try restarting the app.' + }, fallback: { id: 'cboard.components.Settings.Subscribe.fallback', defaultMessage: 'Wait please...' diff --git a/src/components/Settings/Subscribe/SubscriptionInfo.js b/src/components/Settings/Subscribe/SubscriptionInfo.js index 40589f125..f90e62fdd 100644 --- a/src/components/Settings/Subscribe/SubscriptionInfo.js +++ b/src/components/Settings/Subscribe/SubscriptionInfo.js @@ -29,8 +29,8 @@ import { import RefreshIcon from '@material-ui/icons/Refresh'; import IconButton from '../../UI/IconButton'; -import { isAndroid, isElectron } from '../../../cordova-util'; -import { GOOGLE_PLAY_STORE_URL } from './Subscribe.constants'; +import { isAndroid, isElectron, isIOS } from '../../../cordova-util'; +import { APP_STORE_URL, GOOGLE_PLAY_STORE_URL } from './Subscribe.constants'; const propTypes = { ownedProduct: PropTypes.object.isRequired, @@ -131,13 +131,15 @@ const SubscriptionInfo = ({