Skip to content

Call Back Not Hit After Payment Success And Failed In Paymob Sdk #11

Description

@AbdullahAfzal3672

I am integrating the Paymob React Native SDK into my application and facing an issue with the payment callback flow. The payment is being processed successfully, and the transaction is completed from Paymob’s side. However, after success, the SDK modal/sheet remains stuck displaying the raw success JSON response instead of closing or triggering the success listener/callback. Current behavior: • Payment completes successfully • Success response JSON is displayed inside the sheet/modal • The SDK does not dismiss automatically • Success listener / callback is not triggered Example response shown on the sheet: { “success”: true, “message”: “Payment successful”, “data”: { …

Here Is My Code

import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
Image,
ScrollView,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
SafeAreaView,
NativeModules,
} from 'react-native';
import Paymob, { PaymentStatus } from 'paymob-reactnative';
import * as api from '../../api/apiService';
import colors from '../../utils/colors';
import { useNavigation, useRoute, CommonActions } from '@react-navigation/native';
import { usePaymentReserveMutation } from '../../api/rtkApi';
import { getLocalizedText } from '../../utils/language';
import { useLanguage } from '../../context/LanguageContext';
import { formatPrice } from '../../utils/constants';
import { tracking } from '../../utils/tracking';
import CurrencySymbol from '../../components/CurrencySymbol';
import BackButton from '../../components/BackButton';

const PaymentDetailScreen: React.FC = () => {
const { language } = useLanguage();
const isArabic = language === 'ar';

const [step, setStep] = useState<'data' | 'payment'>('data');
const [loading, setLoading] = useState(false);
const [paymentReserveMutation] = usePaymentReserveMutation();

const navigation = useNavigation();
const route = useRoute();
const { car } = route.params || {};

// State flag used to trigger navigation from React's render cycle
// (calling navigation directly from SDK callback can fail on native threads)
const [paymentResult, setPaymentResult] = useState<'success' | 'fail' | null>(null);

// React-side navigation triggered by paymentResult state change
useEffect(() => {
if (paymentResult === 'success') {
// Try to nudge the SDK to close by removing the listener
Paymob.removeSdkListener();
// Forcefully dismiss any native modal (Paymob sheet)
if (NativeModules.DismissModal) {
NativeModules.DismissModal.dismiss();
} else {
console.warn('DismissModal native module is not available. Please rebuild the app and ensure files are added to the Xcode project.');
}
// Forceful stack reset to Success screen (destroys previous stack)
setTimeout(() => {

    navigation.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [{ name: 'PaymentSuccess' }],
      })
    );
  }, 500);
}

}, [paymentResult, navigation]);

// Global SDK configuration on mount
useEffect(() => {
Paymob.setAppName('Tira Cars');
Paymob.setButtonBackgroundColor('#000000');
Paymob.setButtonTextColor('#FFFFFF');
Paymob.setShowTransactionResult(false);
Paymob.setShowConfirmationPage(false);
Paymob.setShowSaveCard(false);
Paymob.setSaveCardDefault(false);
Paymob.setKeyboardHandlingEnabled(true);
}, []);

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [city, setCity] = useState('');

const totalAmount = parseFloat(car?.total_price);
const reserveAmount = parseFloat(car?.reservation_fee);

const handleSubmitData = () => {
if (!firstName.trim() || !lastName.trim() || !email.trim()) {
Alert.alert(
getLocalizedText(language, 'missingInfo'),
getLocalizedText(language, 'fillAllFields'),
);
return;
}
setStep('payment');
};

/* ---------------- HEADER ---------------- */
const renderHeader = () => (
<View
style={[
styles.header,
{ flexDirection: isArabic ? 'row-reverse' : 'row' },
]}
>
<TouchableOpacity
onPress={() => {
if (step === 'payment') {
setStep('data');
} else {
navigation.goBack();
}
}}
style={styles.backButton}
>
<Image
source={require('../../assets/goback.png')}
style={[
styles.backIcon,
{ transform: [{ scaleX: isArabic ? -1 : 1 }] },
]}
/>

  <Text style={styles.headerTitle}>
    {getLocalizedText(language, 'paymentDetails')}
  </Text>

  <View style={{ width: 24 }} />
</View>

);
/* ---------------- STEP 1: DATA ENTRY ---------------- */
const renderDataEntry = () => (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
<ScrollView
contentContainerStyle={[styles.page, isArabic && { direction: 'ltr' }]}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Text
style={[styles.heading, { textAlign: isArabic ? 'right' : 'center' }]}
>
{getLocalizedText(language, 'dataEntry')}

    <View style={styles.box}>
      <Text
        style={{
          fontSize: 14,
          color: 'gray',
          marginTop: 6,
          marginBottom: 4,
          textAlign: isArabic ? 'right' : 'left',
        }}
      >
        {getLocalizedText(language, 'deductedNote')}
      </Text>

      <View style={styles.detailBox}>
        <Text
          style={{
            textAlign: isArabic ? 'right' : 'left',
            color: colors.black,
          }}
        >
          {getLocalizedText(language, 'basePrice')}
        </Text>
        <Text
          style={{
            textAlign: isArabic ? 'right' : 'left',
            color: colors.black,
          }}
        >
          {getLocalizedText(language, 'vatFee')}
        </Text>
        <Text
          style={{
            textAlign: isArabic ? 'right' : 'left',
            color: colors.black,
          }}
        >
          {getLocalizedText(language, 'customDuty')}
        </Text>
        <Text
          style={{
            textAlign: isArabic ? 'right' : 'left',
            color: colors.black,
          }}
        >
          {getLocalizedText(language, 'brokerFee')}
        </Text>
        <Text
          style={{
            textAlign: isArabic ? 'right' : 'left',
            color: colors.black,
          }}
        >
          {getLocalizedText(language, 'insuranceFee')}
        </Text>
      </View>

      <View
        style={[
          styles.row,
          { flexDirection: isArabic ? 'row-reverse' : 'row' },
        ]}
      >
        <Text
          style={[
            styles.rowText,
            { textAlign: isArabic ? 'right' : 'left', color: colors.black },
          ]}
        >
          {getLocalizedText(language, 'totalAmount')}:
        </Text>
        <View
          style={{
            flexDirection: isArabic ? 'row-reverse' : 'row',
            alignItems: 'center',
          }}
        >
          <Text
            style={[
              styles.rowText,
              {
                textAlign: isArabic ? 'left' : 'right',
                color: colors.black,
              },
            ]}
          >
            {formatPrice(totalAmount, '', language)}
          </Text>
          <CurrencySymbol style={{ marginHorizontal: 3 }} />
        </View>
      </View>

      <View
        style={[
          styles.row,
          { flexDirection: isArabic ? 'row-reverse' : 'row' },
        ]}
      >
        <Text
          style={[
            styles.rowText,
            { textAlign: isArabic ? 'right' : 'left', color: colors.black },
          ]}
        >
          {getLocalizedText(language, 'reservationFee')}:
        </Text>
        <View
          style={{
            flexDirection: isArabic ? 'row-reverse' : 'row',
            alignItems: 'center',
          }}
        >
          <Text
            style={[
              styles.rowText,
              {
                textAlign: isArabic ? 'left' : 'right',
                color: colors.black,
              },
            ]}
          >
            {formatPrice(reserveAmount, '', language)}
          </Text>
          <CurrencySymbol style={{ marginHorizontal: 3 }} />
        </View>
      </View>
    </View>

    <Text
      style={[styles.label, { textAlign: isArabic ? 'right' : 'left' }]}
    >
      {getLocalizedText(language, 'deliveryInfo')}
    </Text>
    <TextInput
      placeholder={getLocalizedText(language, 'firstName')}
      style={[styles.input, isArabic && { textAlign: 'right' }]}
      value={firstName}
      onChangeText={setFirstName}
      placeholderTextColor={colors.gray}
    />
    <TextInput
      placeholder={getLocalizedText(language, 'lastName')}
      style={[styles.input, isArabic && { textAlign: 'right' }]}
      value={lastName}
      onChangeText={setLastName}
      placeholderTextColor={colors.gray}
    />

    <TextInput
      placeholder={getLocalizedText(language, 'email')}
      style={[styles.input, isArabic && { textAlign: 'right' }]}
      keyboardType="email-address"
      value={email}
      onChangeText={setEmail}
      placeholderTextColor={colors.gray}
    />
    <Text
      style={[styles.label, { textAlign: isArabic ? 'right' : 'left' }]}
    >
      {getLocalizedText(language, 'city')}
    </Text>
    <TextInput
      placeholder={getLocalizedText(language, 'city')}
      style={[styles.input, isArabic && { textAlign: 'right' }]}
      value={city}
      onChangeText={setCity}
      placeholderTextColor={colors.gray}
    />

    <TouchableOpacity style={styles.bookBtn} onPress={handleSubmitData}>
      <View
        style={{
          flexDirection: isArabic ? 'row-reverse' : 'row',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <Text style={styles.bookText}>
          {getLocalizedText(language, 'reserveCar')}
        </Text>
        <Text style={styles.bookText}></Text>
      </View>
    </TouchableOpacity>
  </ScrollView>
</KeyboardAvoidingView>

);

/* ---------------- STEP 2: PAYMENT ---------------- */

const handlePayment = async () => {
try {
setLoading(true);
const resp = await paymentReserveMutation({
amount: reserveAmount,
car_id: car.id,
billing_data: {
first_name: firstName,
last_name: lastName,
email,
phone_number: '05xxxxxxxx',
},
}).unwrap();
console.log(resp, 'payment response');

  const clientSecret = resp?.data?.client_secret;
  const publicKey = resp?.data?.public_key;
  const merchantOrderId = resp?.data?.merchant_order_id;

  console.log('Payment Keys:', { clientSecret, publicKey, merchantOrderId });

  if (clientSecret && publicKey) {

tracking.track('start_checkout', { amount: reserveAmount, car_id: car.id });
Paymob.setSdkListener((status: PaymentStatus) => {
console.log('Paymob SDK Result:', status);
switch (status) {
case PaymentStatus.SUCCESS:
console.log('Payment Successful');
setPaymentResult('success');
break;
case PaymentStatus.FAIL:
console.log('Payment Failed');
setPaymentResult('fail');
Alert.alert('Payment Failed', 'Transaction was not successful. Please try again.');
break;
case PaymentStatus.PENDING:
console.log('Payment Pending');
// Optionally handle pending
break;
}
});

    Paymob.setAppName('Tira Cars');
    Paymob.setButtonBackgroundColor('#000000');
    Paymob.setButtonTextColor('#FFFFFF');
    Paymob.setShowTransactionResult(false);
    Paymob.setShowConfirmationPage(false); 
     
    Paymob.presentPayVC(clientSecret, publicKey);
  } else {
    Alert.alert('Error', 'Payment initialization failed. Please try again.');
  }
} catch (e) {
  console.error(e, 'payment error');
  Alert.alert(
    getLocalizedText(language, 'error'),
    getLocalizedText(language, 'errorPayment'),
  );
} finally {
  setLoading(false);
}

};

const renderPayment = () => (
<ScrollView
contentContainerStyle={[styles.page, isArabic && { direction: 'ltr' }]}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<Text
style={[styles.heading, { textAlign: isArabic ? 'right' : 'center' }]}
>
{getLocalizedText(language, 'payment')}

  <View
    style={[
      styles.card,
      { flexDirection: isArabic ? 'row-reverse' : 'row' },
    ]}
  >
    <Image
      source={{ uri: car?.images?.[0]?.image }}
      style={{
        width: 120,
        height: 80,
        borderRadius: 8,
        marginHorizontal: 10,
      }}
    />
    <View
      style={{
        flex: 1,
        marginLeft: isArabic ? 0 : 10,
        marginRight: isArabic ? 10 : 0,
      }}
    >
      <Text
        style={[
          styles.cardText,
          {
            textAlign: isArabic ? 'right' : 'left',
            maxWidth: '100%',
            flexShrink: 1,
          },
        ]}
        numberOfLines={2}
        ellipsizeMode="tail"
      >
        {car?.name}
      </Text>
      <Text style={{ fontWeight: 'bold', marginTop: 5 }}>
        {car?.information.brand}
      </Text>
    </View>
  </View>

  <View
    style={{
      flexDirection: isArabic ? 'row-reverse' : 'row',
      alignItems: 'center',
      marginTop: 10,
    }}
  >
    <Text style={{ fontWeight: '600', fontSize: 15 }}>
      {getLocalizedText(language, 'totalOrder')} (1):{' '}
    </Text>
    <Text style={{ fontWeight: 'bold', fontSize: 16, marginHorizontal: 4 }}>
      {formatPrice(reserveAmount, '', language)}
    </Text>
    <CurrencySymbol />
  </View>

  <TouchableOpacity
    style={styles.confirmBtn}
    onPress={handlePayment}
    disabled={loading}
  >
    {loading ? (
      <ActivityIndicator color="#fff" />
    ) : (
      <Text style={{ color: '#fff', fontSize: 18 }}>
        {getLocalizedText(language, 'completePayment')}
      </Text>
    )}
  </TouchableOpacity>
</ScrollView>

);

return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
{renderHeader()}
{step === 'data' && renderDataEntry()}
{step === 'payment' && renderPayment()}

);
};

/* ---------------- STEP PROGRESS ---------------- */
const StepProgress = ({ currentStep }: { currentStep: 'data' | 'payment' }) => {
const { language } = useLanguage();
const isArabic = language === 'ar';
const steps = [
{ label: getLocalizedText(language, 'dataEntry'), key: 'data' },
{ label: getLocalizedText(language, 'payment'), key: 'payment' },
];

const getStepIndex = (key: string) => steps.findIndex(s => s.key === key);

return (
<View
style={[
styles.progressContainer,
isArabic && { flexDirection: 'row-reverse' },
]}
>
{steps.map((step, index) => {
const isActive = currentStep === step.key;
const isCompleted = getStepIndex(currentStep) > index;
return (
<React.Fragment key={step.key}>

<View
style={[
styles.circle,
{
backgroundColor: isActive || isCompleted ? 'black' : '#ccc',
},
]}
/>
<Text
style={{
fontSize: 14,
color: isActive || isCompleted ? 'black' : '#999',
marginTop: 4,
textAlign: 'center',
}}
>
{step.label}


{index < steps.length - 1 && (
<View
style={[
styles.line,
{
backgroundColor:
getStepIndex(currentStep) > index ? 'black' : '#ccc',
},
]}
/>
)}
</React.Fragment>
);
})}

);
};

const styles = StyleSheet.create({
page: { padding: 20, flexGrow: 1 },
header: {
alignItems: 'center',
justifyContent: 'space-between',
padding: 15,
borderBottomWidth: 1,
borderColor: '#eee',
margin: 16,
},
headerTitle: { fontSize: 20, fontWeight: '600', color: '#000' },
heading: { marginTop: 10, fontSize: 22, fontWeight: '600', color: '#000' },
box: {
backgroundColor: colors.white,
borderRadius: 10,
padding: 15,
marginTop: 20,
borderWidth: 1,
borderColor: colors.black,
},
detailBox: {
marginTop: 10,
backgroundColor: '#f7f7f7',
padding: 10,
borderRadius: 6,
borderWidth: 1,
borderColor: '#ccc',
},
row: {
justifyContent: 'space-between',
marginBottom: 2,
marginTop: 10,
},
rowText: {
fontWeight: '500',
},
label: { marginTop: 15, fontWeight: '600', color: '#000' },
input: {
borderWidth: 1,
borderColor: colors.gray,
borderRadius: 8,
padding: 10,
marginTop: 12,
color: colors.black,
backgroundColor: colors.white,
},
bookBtn: {
backgroundColor: 'black',
padding: 15,
borderRadius: 8,
marginTop: 20,
marginBottom: 20,
},
bookText: { color: '#fff', textAlign: 'center', fontWeight: '600' },
phone: {
marginTop: 150,
fontSize: 20,
fontWeight: 'bold',
color: 'brown',
textAlign: 'center',
},
otpRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 40,
},
otpBox: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
width: 40,
height: 50,
textAlign: 'center',
fontSize: 20,
},
confirmBtn: {
backgroundColor: 'black',
padding: 15,
borderRadius: 8,
marginTop: 30,
marginBottom: 20,
alignItems: 'center',
},
card: {
backgroundColor: colors.white,
padding: 10,
borderRadius: 10,
marginTop: 20,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.black,
width: '100%',
maxWidth: '100%',
overflow: 'hidden',
},
cardText: {
fontWeight: 'bold',
flexWrap: 'wrap',
},
cardPrice: {
fontWeight: 'bold',
marginTop: 5,
},
progressContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 20,
},
stepItem: { alignItems: 'center', flex: 1 },
circle: { width: 22, height: 22, borderRadius: 11 },
line: { height: 2, flex: 1, marginHorizontal: 4 },
backButton: { padding: 8 },
backIcon: { width: 24, height: 24, resizeMode: 'contain' },
methodsContainer: { marginTop: 20 },
methodBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 15,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
backgroundColor: '#fff',
},
selectedMethod: { borderColor: 'black', backgroundColor: '#f9f9f9' },
methodIcon: { width: 30, height: 20, resizeMode: 'contain', marginHorizontal: 10 },
methodText: { fontSize: 16, color: '#333', fontWeight: '500' },
radio: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#ccc' },
radioSelected: { borderColor: 'black', backgroundColor: 'black' },
});

export default PaymentDetailScreen;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions