Expo in app purchases implementation issues - react-native

I am struggling a little with getting in app purchases working due to lack of examples that show how all the functionality links together
In my app, I have one in app purchase which basically allows the user to unlock some restricted functionality for life.
So at the start I want to check if the user have purchased the item before. (via get history)
If they have I unlock the functionality.
On my signed APK file (android) made in android studio, I have the following issues:
-the purchase never acknowledges (although it does when run via react-native run-android)
-if you press the purchase button twice the error "already connected to app store appears"
-I don't think its getting the purchase history when running from the signed file (although I can print out the result in the console in debug mode)
I am not entirely sure when to call await InAppPurchases.connectAsync(); so this could be one potential source of issues
So this is my code in my "Inner App" . My App component is just the InnerApp wrapped in the provider component from redux. The inner app contains all the navigation stacks so the purchase listener should be global.
e.g.
export default function App (){
...more code
return(
< Provider store={store} >
< InnerApp />
</ Provider >
}
Inner app code
import * as InAppPurchases from 'expo-in-app-purchases';
export default function InnerApp (){
.....some more code
//gets purchase history
const getHistory = async ()=>{
await InAppPurchases.connectAsync();
let found=false
const { responseCode, results } = await InAppPurchases.getPurchaseHistoryAsync();
if (responseCode === InAppPurchases.IAPResponseCode.OK) {
results.forEach(result => {
if (result.acknowledged) {
found =true
// this is just saving to local storage (in case they are using the app offline)
savePurchaseHistory(true)
}else{
savePurchaseHistory(false)
}
});
}
if( found){
//updates a state in the redux store
dispatch(purchaseIAP() )
}else if(responseCode === IAPResponseCode.USER_CANCELED ){
dispatch(removeIAP() )
savePurchaseHistory(false)
}
await InAppPurchases.disconnectAsync();
}
//listens for purchases
const setUpIAP = async() => {
// Set purchase listener
await InAppPurchases.connectAsync();
await InAppPurchases.setPurchaseListener(({ responseCode, results, errorCode }) => {
// Purchase was successful
if (responseCode === InAppPurchases.IAPResponseCode.OK) {
results.forEach(purchase => {
if (!purchase.acknowledged) {
// Process transaction here and unlock content...
dispatch(purchaseIAP() )
// Then when you're done
InAppPurchases.finishTransactionAsync(purchase, false);
}
});
}
// Else find out what went wrong
if (responseCode === InAppPurchases.IAPResponseCode.USER_CANCELED) {
} else if (responseCode === InAppPurchases.IAPResponseCode.DEFERRED) {
console.log('User does not have permissions to buy but requested parental approval (iOS only)');
} else {
console.warn(`Something went wrong with the purchase. Received errorCode ${errorCode}`);
}
});
}
//The in app stuff is called when the component is mounted
useEffect(() => {
setUpIAP()
getHistory()
}, [ ] })
Further in my app I have a button that calls the following function when pressed
const unlockModes = async () => {
try {
const items = Platform.select({
ios: [
'dev.products.all_modes'
],
android: ['all_modes'],
});
await connectAsync();
const products = await InAppPurchases.getProductsAsync(items);
if (products.results.length > 0) {
await InAppPurchases.purchaseItemAsync("all_modes");
}
} catch (err) {
alert("error occured while trying to purchase: " + err);
}
};

In the end I used the React Native IAP library and I couldn't get the expo one to work.
I think the Expo Version currently might just be bust.

Setting useGooglePlayCache will resolve your problem

Related

React Native Expo In App Purchase send custom variable to setPurchaseListener

I am implementing In App Purchase function in Expo bare workflow using Expo-In-App-Purchase library. In their documentation it says you need to implement setPurchaseListener on a global scope.
That's currently how I have it set up in my App.tsx file:
// Set purchase listener
setPurchaseListener(async ({ responseCode, results, errorCode }) => {
if (responseCode === IAPResponseCode.OK) {
for (const purchase of results!) {
if (!purchase.acknowledged) {
const {
orderId,
purchaseToken,
acknowledged,
transactionReceipt,
productId,
}: InAppPurchase = purchase;
// in android, consumeItem should be set to false to acknowledge the purchase
// in iOS this isn't needed because it's already specified in app store connect
const consumeItem = Platform.OS === "ios";
finishTransactionAsync(purchase, consumeItem).then(
(response) => {
//send call to back end
}
);
}
}
} else if (responseCode === IAPResponseCode.USER_CANCELED) {
//Do nothing. User cancelled
} else {
console.warn(
`Something went wrong with the purchase. Received response code ${responseCode} and errorCode ${errorCode}`
);
//catch all for all errors
if (
errorCode === IAPErrorCode.SERVICE_TIMEOUT ||
errorCode === IAPErrorCode.SERVICE_DISCONNECTED ||
errorCode === IAPErrorCode.SERVICE_UNAVAILABLE
) {
showMessage(`Service unavailable (${errorCode})`, true);
} else {
showMessage(
`Something went wrong with the purchase. Error code (${errorCode}). Please contact us with the error code if this persists`,
true
);
}
}
});
My question is if I have this defined in my App.tsx file, how is it possible to send a custom variable to my back end after a user purchases something? For example, someone can be in multiple leagues, they buy something for a particular league that they are in, how can I send the success back to my back end stating that the purchase was made for a particular league?
Would I just need to add multiple setPurchaseListeners? If that's so, is there a way to stop listening after a user navigates away from a page?

React Native Expo In App Purchase setPurchaseListener position

I am implementing In App Purchase function in Expo bare workflow using Expo-In-App-Purchase library.
In their documentation we need to implement setPurchaseListener on a global scope.
My question would be, anyone know where does global scope means?
Globally typically means in App.js. Using the template from Expo Documentation, it should look like something below in your App.js file (I am using class components).
setUpIAP = async() => {
// Set purchase listener
await InAppPurchases.connectAsync();
await InAppPurchases.setPurchaseListener(({ responseCode, results, errorCode }) => {
// Purchase was successful
if (responseCode === InAppPurchases.IAPResponseCode.OK) {
results.forEach(purchase => {
if (!purchase.acknowledged) {
console.log(`Successfully purchased ${purchase.productId}`);
// Process transaction here and unlock content...
// Then when you're done
InAppPurchases.finishTransactionAsync(purchase, true);
}
});
}
// Else find out what went wrong
if (responseCode === InAppPurchases.IAPResponseCode.USER_CANCELED) {
console.log('User canceled the transaction');
} else if (responseCode === InAppPurchases.IAPResponseCode.DEFERRED) {
console.log('User does not have permissions to buy but requested parental approval (iOS only)');
} else {
console.warn(`Something went wrong with the purchase. Received errorCode ${errorCode}`);
}
});
}
componentDidMount() {
this.setUpIAP();
}

React Native Google Sign-In (#react-native-community/google-signin). error.code === statusCodes.IN_PROGRESS

I have successfully implemented google-signin into my RN app with Firebase using code below.
When I click button first time it creates FB account....then subsequent times it logs me in.
Everything works fine.....but then from time to time when I click to log in, it returns the error code of statusCodes.IN_PROGRESS
I want to be able to handle this error (by terminating sign-in or something??) but have no idea how.....can anyone help?
p.s. exact line of code causing error = const {accessToken, idToken} = await GoogleSignin.signIn();.....its also giving me this warning Warning: previous promise did not settle and was overwritten. You've called "signIn" while "signIn" was already in progress and has not completed yet.
import firebase from '#react-native-firebase/app';
import auth from '#react-native-firebase/auth';
import {GoogleSignin, statusCodes} from '#react-native-community/google-signin';
import {LoginManager, AccessToken} from 'react-native-fbsdk';
export const _getFbGoogleCredential = async () => {
await GoogleSignin.hasPlayServices();
const {accessToken, idToken} = await GoogleSignin.signIn();
const firebaseCredential = await auth.GoogleAuthProvider.credential(idToken, accessToken);
return firebaseCredential;
};
export const _googleSignIn = async () => {
try {
const firebaseCredential = await _getFbGoogleCredential();
const fbUserObj = await auth().signInWithCredential(firebaseCredential);
} catch (error) {
if (error.code === 'auth/account-exists-with-different-credential') {
//xx
} else if (error.code === statusCodes.SIGN_IN_CANCELLED) {
//xx
} else if (error.code === statusCodes.IN_PROGRESS) {
//HERE IS THE ISSUE
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
//xx
} else {
//xx
}
}
};
I also had this same error
Warning: previous promise did not settle and was overwritten. You've called "signIn" while "signIn" was already in progress and has not completed yet.
and i was able to fix is by NOT using GoogleSigninButton that comes with the package #react-native-google-signin/google-signin
The GoogleSigninButton has a bug so just use any other button component...weird fix but hey it works. Behold the culprit

Ionic React - Navigate to a page when an FCM notification is tapped

I am implementing FCM notifications in an Ionic React application. I am having trouble navigating to another page to display the notification details.
I have created a FCMService class in my react App, and initialising this in the index.ts file.
// FCMService.ts
export default class FCMService {
public static Instance: FCMService;
private _store: Store<IAppState>;
constructor(store: Store<IAppState>) {
this._store = store;
}
public static Initalise(store: Store<IAppState>) {
if (!FCMService.Instance) {
FCMService.Instance = new FCMService(store);
FCMService.Instance.InitaliseFCM();
FCMService.Instance._store.subscribe(() => { console.log(store.getState()) });
} else {
console.debug("FCM service already intialised. Please use FCMService.Instance");
}
}
private InitaliseFCM() {
// Request permission to use push notifications
// iOS will prompt user and return if they granted permission or not
// Android will just grant without prompting
PushNotifications.requestPermission().then(result => {
console.log(result);
if (result.granted) {
// Register with Apple / Google to receive push via APNS/FCM
PushNotifications.register();
} else {
// Show some error
}
});
// On success, we should be able to receive notifications
PushNotifications.addListener('registration', (token: PushNotificationToken) => {
console.log(token);
localStorage.setItem("FCM_TOKEN", token.value);
}
);
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError',
(error: any) => {
console.log(error);
}
);
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived',
(notification: PushNotification) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
}
);
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(notification: PushNotificationActionPerformed) => {
console.log(notification);
let data = notification.notification.data as INotificationData;
this._store.dispatch(setNotificationActionCreator(data));
}
);
}
}
and then the index.ts
const store = configureStore();
interface MainProps {
store: Store<IAppState>;
}
FCMService.Initalise(store);
ReactDOM.render(<Provider store={store}><App /> </Provider>, document.getElementById('root'));
serviceWorker.unregister();
I even tried using the Redux store to save the notification on Tap - and then that would publish the notification change event (which might of worked - if I could access the useHistory() hook in the App.tsx file)
This was my attempt at navigating via Redux store in App.tsx
const App: React.FC<IProps> = ({ getCompanies, getUser, notification }) => {
console.log('app');
console.log(process.env);
const history = useHistory();
if(notification){
history.push(`/page/plot-position/{notification.id}`);
}
return (
<IonApp>
<IonReactRouter>
<IonSplitPane contentId="main" when="false">
<Menu />
<IonRouterOutlet id="main">
<Route path="/login" component={LoginPage} exact />
<PrivateRoute path="/page/plot-position/:notificationId/" component={PlotPositionPage} exact />
<Redirect from="/" to="/login" exact />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
};
const mapStateToProps = (store: IAppState) => {
return {
user: store.user.user as UserDTO,
notification: store.notificationState.notification
};
};
const mapDispatchToProps = (dispatch: any) => {
return {
getCompanies: () => dispatch(getCompaniesStartActionCreator()),
getUser: () => dispatch(getUserStartActionCreator())
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
It looks like your navigation works, but you're having trouble passing the notification object through to the page? You can pass the object through history state.
To access the useHistory hook you would need to make your FCMService a custom hook.
const useFCMService = (): void => {
const history = useHistory();
React.useEffect(() => {
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed',
(action: PushNotificationActionPerformed) => {
const notification = action.notification.data as INotificationData;
history.push({ pathname: '/page/plot-position/', state: { notification } });
}
);
}, []);
}
And then include your useFCMService custom hook in your App component.
const App: React.FC<IProps> = ({ getCompanies, getUser }) => {
useFCMService();
...
};
Deep linking provides us a way to do this: Using both an action to open the application and an action at opening the application we can enroute the user to the correct destination.
Opening the application
Here we will create an action to open the url when the user taps on the push notification; to do this less use a listener:
const {PushNotifications, App} = Plugins
***
PushNotifications.addListener(
"pushNotificationActionPerformed",
(notification: PushNotificationActionPerformed) =>{
const data = notification.notification.data;
if (data.packageNumber) App.openUrl({url: `com.company.appname://tabs/package-details/${data.packageNumber}`})
else App.openUrl({url:'/tabs'})
}
)
com.company.app:// is of capital importance since the app must reach the application must reach an existing given url, otherwise the following action(catching the url) won't be triggers since it waits a complete true from the App.openUrl function; as we are opening an internal url, this must begin with the apps given name in the capacitor config page(see the following example where we can realize how use the local url).
In this way we are adding a function to open the application in an specific route.
Redirecting the user
Here, we will complete the application's part from the deep linking tutorial: we create a new listener component who handles the appOpenUrl events and redirects to the user and we will put it on the main App file inside of its respective IonRouter:
const AppUrlListener: React.FC<any> = () => {
let history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (data: any) => {
const slug = data.url.split(':/').pop();
if (slug) {
history.push(slug);
}
});
}, []);
return null;
};
Don't forget the route in router must begin with /, and since the application url contains :/, we split the url here and we get the second part, the slug; we push it on the history, triggering the router and getting the normal behaviour when you entering in a new route.
We will add this component inside of the router:
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu />
<AppUrlListener />
<IonRouterOutlet id="main">
Now, the application will be listening the appOpenUrl event, and when it gets a new of this events, it will push the gotten url to the history, redirecting the user to that route.

React-Native/Expo Location status if it user don't want to allow location after giving permission to app

After allowing permission to access GPS in react native App. If the user rejected to turn on the gps. It will show errors like
Unhandled promise rejection: Error: Location request failed due to unsatisfied device settings."
I want to avoid if the user rejects the Gps turn on option It will return something. so I need If condition for the location whether it is On or Off. (I'm using expo-location)
You're seeing this error because the Location.getCurrentPositionAsync is async method and it returns a promise and if it fails it throws an error (the error you're seeing).
You could wrap your code inside a try and catch block to catch the error and do something about it.
Example:
_getLocationAsync = async () => {
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
alert('The request was denied');
}else{
try{
let location = await Location.getCurrentPositionAsync({});
// do something with location
}catch(e){
alert('We could not find your position. Please make sure your location service provider is on');
console.log('Error while trying to get location: ', e);
}
}
}
// call this._getLocationAsync();
you will need to check the status from expo-location and redirect user to settings to allow the permission for that you can use android intents for android and for ios you can use Linking to redirect the user to device settings and give permissions
requestLocationPermission = async () => {
const { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status === 'granted) {
navigation.navigate('screen_name');
} else {
// Alert Message if user does not allow permissions
Alert.alert("alert Message", "Instructions based on OS", [
{
text: 'Open Settings',
onPress: () => goToSettings(),
style: 'cancel',
},
{ text: Languages.DENY, onPress: () => navigation.goback()},
]);
}
};
go to settings
goToSettings = () => {
if (Platform.OS == 'ios') {
// Linking for iOS
Linking.openURL('app-settings:');
} else {
// IntentLauncher for Android
IntentLauncher.startActivityAsync(
IntentLauncher.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS
);
}
};
NOTE Intent launcher is a separate package for android
use this to turn gps on:
Location.enableNetworkProviderAsync()
It's a simple popup. (without redirection to a setting)