Skip to content

Commit

Permalink
fix: improve visualization and management of IAPs
Browse files Browse the repository at this point in the history
  • Loading branch information
outaTiME committed Jan 8, 2024
1 parent 27f5fd6 commit ce7141e
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 70 deletions.
16 changes: 12 additions & 4 deletions packages/client/components/AppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -925,15 +925,23 @@ const withUserActivity = (Component) => (props) => {
};

const withPurchases = (Component) => (props) => {
const [, setPurchasesConfigured] = Helper.useSharedState(
'purchasesConfigured',
false,
);
React.useEffect(() => {
const configure = async () => {
__DEV__ && (await Purchases.setLogLevel(Purchases.LOG_LEVEL.VERBOSE));
Purchases.configure({ apiKey: Settings.REVENUECAT_API_KEY });
if (__DEV__) {
console.log('🤑 Purchases configured');
}
};
configure();
configure()
.then(() => {
if (__DEV__) {
console.log('🤑 Purchases configured');
}
setPurchasesConfigured(true);
})
.catch(console.warn);
}, []);
return <Component {...props} />;
};
Expand Down
7 changes: 6 additions & 1 deletion packages/client/components/CardItemView.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default ({
const { theme, fonts } = Helper.useTheme();
const CardContainer =
useSwitch !== true && !!onAction && loading === false ? RectButton : View;
const addSpacer = (onAction && (chevron || check)) || useSwitch || !!value;
return (
<>
<CardContainer
Expand Down Expand Up @@ -109,6 +110,7 @@ export default ({
flexDirection: 'row',
alignItems: 'center',
paddingVertical: Settings.PADDING,
// gap: Settings.PADDING,
},
drag && {
marginLeft: Settings.PADDING,
Expand All @@ -121,8 +123,11 @@ export default ({
) : (
<View
style={[
{
addSpacer && {
marginRight: Settings.PADDING,
},
{
// flex: 1,
flexShrink: 0,
flexGrow: 1,
},
Expand Down
10 changes: 7 additions & 3 deletions packages/client/config/I18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const i18n = new I18n({
retry: 'Reintentar',
developer: 'Desarrollador',
text_copied: 'Texto copiado al portapapeles',
generic_error: 'Imposible completar la operación seleccionada.',
// rate detail
detail: 'Detalle',
one_week: '1S',
Expand Down Expand Up @@ -58,6 +59,9 @@ const i18n = new I18n({
device_identifier: 'Identificador',
opts_statistics: 'Estadísticas',
opts_support: 'Soporte y difusión',
opts_support_note:
// 'Con tu colaboración haces que la aplicación siga creciendo y mejorando.',
'Tu apoyo impulsa el desarrollo y mantenimiento continuo de la aplicación.',
send_app_feedback: 'Enviar comentarios',
leave_app_review: 'Dejar una reseña',
share: 'Compartir',
Expand Down Expand Up @@ -118,10 +122,10 @@ const i18n = new I18n({
select_rates: 'Seleccionar',
// no_selected_rates: 'Agregá las cotizaciones que deseas visualizar.',
no_selected_rates: 'No se han seleccionado cotizaciones a visualizar.',
purchase_error: 'Imposible completar la operación seleccionada.',
purchase_success: '¡Gracias por tu donación!',
// donate
/* purchase_success: '¡Gracias por tu donación!',
purchase_success_message:
'Tu apoyo impulsa el desarrollo y mantenimiento continuo de la aplicación.',
'Tu apoyo impulsa el desarrollo y mantenimiento continuo de la aplicación.', */
},
});

Expand Down
206 changes: 150 additions & 56 deletions packages/client/screens/SettingsScreen.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { useFocusEffect } from '@react-navigation/native';
import { compose } from '@reduxjs/toolkit';
import * as Device from 'expo-device';
import * as MailComposer from 'expo-mail-composer';
import React from 'react';
import { Linking, Share, Alert } from 'react-native';
import {
Linking,
Share,
View,
Text,
ActivityIndicator,
Alert,
} from 'react-native';
import Purchases from 'react-native-purchases';
import { useSelector, shallowEqual } from 'react-redux';

Expand All @@ -17,6 +25,52 @@ import DateUtils from '../utilities/Date';
import Helper from '../utilities/Helper';
import Sentry from '../utilities/Sentry';

const PriceBadge = ({ price, currencyCode, loading }) => {
const { theme, fonts } = Helper.useTheme();
return (
<View
style={[
{
borderColor: Settings.getStrokeColor(theme, false),
borderWidth: 1,
borderRadius: Settings.BORDER_RADIUS / 2,
paddingVertical: 2,
paddingHorizontal: 6,
justifyContent: 'center',
marginVertical: -Settings.PADDING,
},
loading && {
borderColor: 'transparent',
},
]}
>
<Text
style={[
fonts.body,
{
color: Settings.getGrayColor(theme),
textAlign: 'right',
},
loading && {
opacity: 0,
},
]}
numberOfLines={1}
>
{currencyCode} {Helper.getCurrency(price)}
</Text>
{loading && (
<ActivityIndicator
animating
color={Settings.getForegroundColor(theme)}
size="small"
style={{ position: 'absolute', alignSelf: 'center' }}
/>
)}
</View>
);
};

const SettingsScreen = ({ headerHeight, tabBarheight, navigation }) => {
const { updatedAt, appearance } = useSelector(
({ rates: { updated_at: updatedAt }, application: { appearance } }) => ({
Expand Down Expand Up @@ -45,43 +99,81 @@ const SettingsScreen = ({ headerHeight, tabBarheight, navigation }) => {
const onPressReview = React.useCallback(() => {
Linking.openURL(Settings.APP_REVIEW_URI).catch(console.warn);
}, []);
const onPressShare = React.useCallback(() => {
Share.share({
message: I18n.t('share_message', {
appName: Settings.APP_NAME,
websiteUrl: Settings.WEBSITE_URL,
}),
}).catch(console.warn);
}, []);
const [contactAvailable] = Helper.useSharedState('contactAvailable', false);
const [storeAvailable] = Helper.useSharedState('storeAvailable', false);
const [tick, setTick] = React.useState();
const updatedAtFromNow = React.useMemo(
() => DateUtils.get(updatedAt).calendar(),
[tick],
);
const tickCallback = React.useCallback(
(tick) => {
setTick(tick);
},
[updatedAt],
);
Helper.useInterval(tickCallback);
// donate
const getPurchaseProduct = React.useCallback(
() =>
Helper.timeout(
Purchases.getProducts(
['small_contribution'],
Purchases.PRODUCT_CATEGORY.NON_SUBSCRIPTION,
),
)
.then((products) => products?.[0])
.catch(console.warn),
[],
);
const [purchaseProduct, setPurchaseProduct] = React.useState();
React.useEffect(() => {
if (__DEV__ && purchaseProduct) {
console.log(
'🎟️ Product to donate updated',
JSON.stringify(purchaseProduct),
);
}
}, [purchaseProduct]);
useFocusEffect(
React.useCallback(() => {
// initial product fetch
if (!purchaseProduct) {
getPurchaseProduct().then((product) => {
if (product) {
setPurchaseProduct(product);
}
});
}
}, [purchaseProduct]),
);
const [donateLoading, setDonateLoading] = React.useState(false);
const onPressDonate = React.useCallback(() => {
setDonateLoading(true);
Purchases.getProducts(
['small_contribution'],
Purchases.PRODUCT_CATEGORY.NON_SUBSCRIPTION,
)
.then((products) => {
const product = products[0];
Promise.resolve(purchaseProduct)
.then((product) => product ?? getPurchaseProduct())
.then((product) => {
if (product) {
// force an update in case the product changes
setPurchaseProduct(product);
return Purchases.purchaseStoreProduct(product);
}
throw new Error('No products available');
})
.then((opts) => {
Alert.alert(
I18n.t('purchase_success'),
I18n.t('purchase_success_message'),
[
{
text: I18n.t('accept'),
onPress: () => {
// pass
},
},
],
{
cancelable: false,
},
);
})
.catch((e) => {
// silent ignore on user cancellation
if (!e.userCancelled) {
Sentry.captureException(new Error('Purchase error', { cause: e }));
Alert.alert(
I18n.t('purchase_error'),
I18n.t('generic_error'),
'',
[
{
Expand All @@ -100,29 +192,8 @@ const SettingsScreen = ({ headerHeight, tabBarheight, navigation }) => {
.finally(() => {
setDonateLoading(false);
});
}, []);
const onPressShare = React.useCallback(() => {
Share.share({
message: I18n.t('share_message', {
appName: Settings.APP_NAME,
websiteUrl: Settings.WEBSITE_URL,
}),
}).catch(console.warn);
}, []);
const [contactAvailable] = Helper.useSharedState('contactAvailable', false);
const [storeAvailable] = Helper.useSharedState('storeAvailable', false);
const [tick, setTick] = React.useState();
const updatedAtFromNow = React.useMemo(
() => DateUtils.get(updatedAt).calendar(),
[tick],
);
const tickCallback = React.useCallback(
(tick) => {
setTick(tick);
},
[updatedAt],
);
Helper.useInterval(tickCallback);
}, [purchaseProduct]);
const [purchasesConfigured] = Helper.useSharedState('purchasesConfigured');
return (
<FixedScrollView
{...{
Expand Down Expand Up @@ -180,14 +251,37 @@ const SettingsScreen = ({ headerHeight, tabBarheight, navigation }) => {
/>
)}
</CardView>
<CardView title={I18n.t('opts_support')} plain>
<CardItemView
title={I18n.t('donate')}
useSwitch={false}
chevron={false}
onAction={onPressDonate}
loading={donateLoading}
/>
<CardView
title={I18n.t('opts_support')}
plain
note={I18n.t('opts_support_note')}
>
{purchasesConfigured && (
<CardItemView
title={I18n.t('donate')}
useSwitch={false}
chevron={false}
onAction={onPressDonate}
loading={donateLoading}
{...(purchaseProduct && {
value: `${Helper.getCurrency(purchaseProduct.price, true, true)}`,
})}
// large opt
/* titleDetail={I18n.t('donate_detail')}
{...(!donateLoading && {
onAction: onPressDonate,
})}
{...(purchaseProduct && {
value: (
<PriceBadge
price={purchaseProduct.price}
currencyCode={purchaseProduct.currencyCode}
loading={donateLoading}
/>
),
})} */
/>
)}
{contactAvailable && (
<CardItemView
title={I18n.t('send_app_feedback')}
Expand Down
15 changes: 9 additions & 6 deletions packages/client/screens/StatisticsScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const StatisticsScreen = ({ headerHeight, tabBarheight }) => {
.catch(console.warn)
.finally(() => setLoading(false));
}, []);
const [purchasesConfigured] = Helper.useSharedState('purchasesConfigured');
return (
<FixedScrollView
{...{
Expand Down Expand Up @@ -94,12 +95,14 @@ const StatisticsScreen = ({ headerHeight, tabBarheight }) => {
useSwitch={false}
value={Helper.formatIntegerNumber(sharedRates)}
/>
<CardItemView
title={I18n.t('app_donations')}
useSwitch={false}
value={donations}
loading={Loading}
/>
{purchasesConfigured && (
<CardItemView
title={I18n.t('app_donations')}
useSwitch={false}
value={donations}
loading={Loading}
/>
)}
{lastReview && (
<CardItemView
title={I18n.t('app_last_review')}
Expand Down

0 comments on commit ce7141e

Please sign in to comment.