Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: billing #636

Merged
merged 14 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/workflows/android-weekly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false

# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: false

- name: Get branch name
run: |
# Short name for current branch. For PRs, use target branch (base ref)
Expand Down
16 changes: 0 additions & 16 deletions .github/workflows/play-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,6 @@ jobs:
- name: clean generated builds
run: rm -rf android/app/build/generated/

- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false

# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: false

- name: gradle build AAB
run: cd android && chmod +x gradlew && ./gradlew bundleRelease

Expand Down

This file was deleted.

55 changes: 0 additions & 55 deletions .yarn/patches/react-native-npm-0.76.1-c738a2fa39.patch

This file was deleted.

2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true
newArchEnabled=false

# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
Expand Down
11 changes: 1 addition & 10 deletions android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,4 @@ include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')

apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()

includeBuild('../node_modules/react-native') {
dependencySubstitution {
substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid"))
substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid"))
substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
}
}
useExpoModules()
14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@
"@react-native-cookies/cookies": "^6.2.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-native/babel-preset": "^0.76.1",
"@react-native/gradle-plugin": "patch:@react-native/gradle-plugin@npm%3A0.76.1#~/.yarn/patches/@react-native-gradle-plugin-npm-0.76.1-f19c439d00.patch",
"@react-native/gradle-plugin": "^0.76.1",
"@react-native/metro-config": "^0.76.1",
"@react-native/typescript-config": "0.76.1",
"@react-navigation/drawer": "^7.0.1",
"@react-navigation/material-top-tabs": "^7.0.1",
"@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0",
"@sentry/react-native": "^6.1.0",
"@sharcoux/slider": "8.0.4",
"@sharcoux/slider": "8.0.5",
"@shopify/flash-list": "^1.7.2",
"@shopify/react-native-skia": "1.5.3",
"axios": "^1.7.7",
Expand All @@ -61,7 +61,7 @@
"deepmerge": "^4.3.1",
"dropbox": "git+https://lovegaoshi@github.com/lovegaoshi/dropbox-sdk-js.git",
"event-target-polyfill": "^0.0.4",
"expo": "^52.0.2",
"expo": "^52.0.4",
"expo-auth-session": "~6.0.0",
"expo-clipboard": "~7.0.0",
"expo-crypto": "~14.0.1",
Expand All @@ -86,7 +86,7 @@
"qs": "^6.13.0",
"react": "18.3.1",
"react-i18next": "^15.1.1",
"react-native": "patch:react-native@npm%3A0.76.1#~/.yarn/patches/react-native-npm-0.76.1-c738a2fa39.patch",
"react-native": "^0.76.1",
"react-native-app-auth": "^8.0.0",
"react-native-background-timer": "git+https://github.com/lovegaoshi/react-native-background-timer.git",
"react-native-blob-jsi-helper": "^0.3.1",
Expand All @@ -107,7 +107,7 @@
"react-native-screens": "4.0.0",
"react-native-shadow-2": "^7.1.1",
"react-native-share-menu": "git+https://github.com/Expensify/react-native-share-menu.git",
"react-native-svg": "15.8.0",
"react-native-svg": "15.9.0",
"react-native-svga-player": "https://lovegaoshi@github.com/lovegaoshi/react-native-svga-player.git#commit=f8c6303fddb528a1a94d2ab4696c9318c0002cfd",
"react-native-text-ticker": "https://lovegaoshi@github.com/lovegaoshi/react-native-text-ticker.git#commit=b9eb454b18bb621a769ce4b57b4603a81501b477",
"react-native-track-player": "https://github.com/lovegaoshi/react-native-track-player.git#commit=6142378143e43cf28b74940195b1def6e0ec795d",
Expand All @@ -124,9 +124,7 @@
"zustand": "^5.0.1"
},
"resolutions": {
"metro": "^0.81.0",
"react-native@npm:*": "patch:react-native@npm%3A0.76.1#~/.yarn/patches/react-native-npm-0.76.1-c738a2fa39.patch",
"@react-native/gradle-plugin@npm:0.76.1": "patch:@react-native/gradle-plugin@npm%3A0.76.1#~/.yarn/patches/@react-native-gradle-plugin-npm-0.76.1-f19c439d00.patch"
"metro": "^0.81.0"
},
"devDependencies": {
"@babel/core": "^7.26.0",
Expand Down
13 changes: 0 additions & 13 deletions patches/@sharcoux+slider+8.0.4.patch

This file was deleted.

25 changes: 0 additions & 25 deletions patches/react-native-tab-view+4.0.1.patch

This file was deleted.

4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import MainBackground from './components/background/MainBackground';
import useTheme from './hooks/useTheme';
// eslint-disable-next-line import/no-unresolved
import { TRACKING } from '@env';
import useVIP from './hooks/useVIP';
import { useSetupVIP } from './hooks/useVIP';

if (TRACKING) {
Sentry.init({
Expand Down Expand Up @@ -52,7 +52,7 @@ const APM = ({ PIP, isLandscape }: { PIP: boolean; isLandscape: boolean }) => {
};

export default function App(appProps: NoxComponent.AppProps) {
const { vip } = useVIP();
const { vip } = useSetupVIP();
const isSplashReady = useSplash(
__DEV__ || appProps.intentData || vip ? 1 : 2500,
);
Expand Down
131 changes: 131 additions & 0 deletions src/components/billing/View.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { View, StyleSheet } from 'react-native';
import React, { useState } from 'react';
import { Text, Button, ActivityIndicator } from 'react-native-paper';
import { useTranslation } from 'react-i18next';
import { Image } from 'expo-image';
import Purchases from 'react-native-purchases';

import useVIP, { checkGuardVIP, purchaseVIP } from '@hooks/useVIP';
import { styles } from '../style';
import logger from '@utils/Logger';

interface LoadingChildrenProps {
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
interface LoadingWrapperProps {
Child: (p: LoadingChildrenProps) => JSX.Element;
}

const LoadingIconWrapper = ({ Child }: LoadingWrapperProps) => {
const [loading, setLoading] = useState(false);
if (loading) {
return <ActivityIndicator />;
}
return <Child setLoading={setLoading} />;
};

const BiliVIP = ({ setLoading }: LoadingChildrenProps) => {
const { t } = useTranslation();

const checkBiliVIP = async () => {
setLoading(true);
await checkGuardVIP();
setLoading(false);
};

return <Button onPress={checkBiliVIP}>{t('Billing.NoxAuth')}</Button>;
};
Comment on lines +27 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and user feedback for VIP check.

The current implementation lacks error handling and doesn't provide feedback to users about the success or failure of the VIP check operation.

 const BiliVIP = ({ setLoading }: LoadingChildrenProps) => {
   const { t } = useTranslation();
 
   const checkBiliVIP = async () => {
     setLoading(true);
+    try {
-      await checkGuardVIP();
+      const result = await checkGuardVIP();
+      if (result) {
+        // Show success message
+      }
+    } catch (error) {
+      logger.error('BiliVIP check failed:', error);
+      // Show error message to user
+    } finally {
       setLoading(false);
+    }
   };
 
   return <Button onPress={checkBiliVIP}>{t('Billing.NoxAuth')}</Button>;
 };

Committable suggestion skipped: line range outside the PR's diff.


const RevenueCatVIP = ({ setLoading }: LoadingChildrenProps) => {
const { t } = useTranslation();
const checkRevenueCatVIP = async () => {
setLoading(true);
try {
const offerings = await Purchases.getOfferings();
if (
offerings.current !== null &&
offerings.current.availablePackages.length !== 0
) {
const { customerInfo } = await Purchases.purchasePackage(
offerings.current.availablePackages[0],
);
if (
typeof customerInfo.entitlements.active['apm-pro'] !== 'undefined'
) {
purchaseVIP();
}
}
} catch (e) {
logger.error(JSON.stringify(e));
}
setLoading(false);
};

return (
<Button onPress={checkRevenueCatVIP}>{t('Billing.RevenueCat')}</Button>
);
};
Comment on lines +39 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve RevenueCat purchase flow implementation.

Several improvements can be made to enhance code quality and user experience:

  1. Extract magic strings to constants
  2. Simplify nested conditions
  3. Add user feedback for errors
+const VIP_ENTITLEMENT_ID = 'apm-pro';
+
 const RevenueCatVIP = ({ setLoading }: LoadingChildrenProps) => {
   const { t } = useTranslation();
+
+  const hasAvailableOfferings = (offerings: Purchases.Offerings) =>
+    offerings.current?.availablePackages.length > 0;
+
   const checkRevenueCatVIP = async () => {
     setLoading(true);
     try {
       const offerings = await Purchases.getOfferings();
-      if (
-        offerings.current !== null &&
-        offerings.current.availablePackages.length !== 0
-      ) {
-        const { customerInfo } = await Purchases.purchasePackage(
-          offerings.current.availablePackages[0],
-        );
-        if (
-          typeof customerInfo.entitlements.active['apm-pro'] !== 'undefined'
-        ) {
-          purchaseVIP();
-        }
+      if (!hasAvailableOfferings(offerings)) {
+        throw new Error(t('Billing.NoOfferings'));
       }
+      
+      const { customerInfo } = await Purchases.purchasePackage(
+        offerings.current!.availablePackages[0],
+      );
+      
+      if (customerInfo.entitlements.active[VIP_ENTITLEMENT_ID]) {
+        await purchaseVIP();
+        // Show success message
+      }
     } catch (e) {
       logger.error(JSON.stringify(e));
+      // Show error message to user
+      Alert.alert(t('Billing.PurchaseError'), e.message);
+    } finally {
+      setLoading(false);
     }
-    setLoading(false);
   };

Committable suggestion skipped: line range outside the PR's diff.


const PurchaseVIPScreen = () => {
const { t } = useTranslation();
return (
<View style={mStyle.container}>
<Image
style={mStyle.azusaBeg}
source={{
uri: 'https://img.nga.178.com/attachments/mon_202201/31/-zue37Q2p-ixpkXsZ7tT3cS9y-af.gif',
}}
/>
<Text>{t('Billing.PremiumFeaturesIntro')}</Text>
<View style={styles.alignMiddle}>
<LoadingIconWrapper Child={BiliVIP} />
<LoadingIconWrapper Child={RevenueCatVIP} />
</View>
<Text>{t('Billing.NoxFans')}</Text>
</View>
);
};
Comment on lines +69 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move hardcoded URLs to configuration and add image error handling.

The hardcoded image URL should be moved to a configuration file, and error handling should be added for image loading.

+// Add to a config file
+export const IMAGES = {
+  AZUSA_BEG: 'https://img.nga.178.com/attachments/mon_202201/31/-zue37Q2p-ixpkXsZ7tT3cS9y-af.gif'
+};
+
 const PurchaseVIPScreen = () => {
   const { t } = useTranslation();
+  const [imageError, setImageError] = useState(false);
+
   return (
     <View style={mStyle.container}>
       <Image
         style={mStyle.azusaBeg}
         source={{
-          uri: 'https://img.nga.178.com/attachments/mon_202201/31/-zue37Q2p-ixpkXsZ7tT3cS9y-af.gif',
+          uri: IMAGES.AZUSA_BEG,
         }}
+        onError={() => setImageError(true)}
       />
+      {imageError && (
+        <Text style={styles.errorText}>{t('Common.ImageLoadError')}</Text>
+      )}

Committable suggestion skipped: line range outside the PR's diff.


const VIPScreen = () => {
const { t } = useTranslation();

return (
<View style={mStyle.container}>
<Image
style={mStyle.azusaMock}
source={{
uri: 'https://img.nga.178.com/attachments/mon_202202/01/-zue37Q2p-6rfwK2dT1kShs-b4.jpg',
}}
/>
<Text style={styles.centerText}>{t('Billing.thankU')}</Text>
<Text style={styles.centerText}>{t('Billing.godBlessU')}</Text>
<Text style={styles.centerText}>{t('Billing.urVeryVeryGorgeous')}</Text>
</View>
);
};

export default () => {
const vip = useVIP(state => state.VIP);

if (vip) {
return <VIPScreen />;
}
return <PurchaseVIPScreen />;
};

const mStyle = StyleSheet.create({
container: { flex: 1, paddingHorizontal: 10, paddingTop: 10 },
text: {
fontSize: 20,
},
azusaBeg: {
width: '100%',
height: '40%',
alignSelf: 'center',
},
azusaMock: {
width: '100%',
height: '30%',
alignSelf: 'center',
},
});
4 changes: 0 additions & 4 deletions src/components/billing/bill.ts

This file was deleted.

Loading
Loading