React Native: App Store Rejection, Support for IPv6
Asked Answered
A

1

5

I have a simple react-native client for a website, in login page it provides two option, enter login code manually or scan it using barcode scanner. i've tested the app in real device and emulator lot's of times it's working fine. Actually i test only over ipv4, and for login im using fetch, which i think is supporting ipv6 by default.

They say over ipv6 network when app was offline, i cannot understand what does it mean to be OFFLINE and be on IPV6 network?

When app is offline, i'm showing error to user that there is no connectivity. so i don't know how it can crash.

should adding a timeout to fetch request fix the issue?

But the app being rejected 3 times due to same error :

Performance - 2.1

Thank you for your resubmission.

Your app crashes on iPhone running iOS 9.3.3 connected to an IPv6 network when we:

Specifically, tapping the login still leads the app to crash.

This occurred when your app was used:

  • Offline
  • On Wi-Fi

We have attached detailed crash logs to help troubleshoot this issue.

Here is the login.js :

'use strict';
import React, { Component } from 'react';
import {
  Text,
  View,
  Image,
  TextInput,
  TouchableOpacity,
  ActivityIndicatorIOS,
  StyleSheet,
  Dimensions,
  AlertIOS,
  NetInfo,
 } from 'react-native';
import Camera from 'react-native-camera';
var { width, height } = Dimensions.get('window');

class Login extends Component {
    constructor(props){
        super(props);
        this.state = {
            showProgress: false,
            showCamera: false,
            cameraType: Camera.constants.Type.back,
            barcode: true,
            isConnected: false,
        }
    }

    componentWillMount(){
      NetInfo.isConnected.fetch().done((data) => {
      this.setState({
        isConnected: data
      })
    });
    }

    _onBarCodeRead(e) {
    this.setState({
      showCamera: false,
      barcodeData: e.data,
      logincode: e.data,
      success: true,
    });
    this.onLoginPressed();
    }

    render(){

        if(this.state.showCamera) {
        return (
            <Camera
                ref="cam"
                style={styles.container}
                onBarCodeRead={this._onBarCodeRead.bind(this)}
                type={this.state.cameraType}>
            </Camera>
        );
        } else {
          var errorCtrl = <View />;
          if(!this.state.success){
              errorCtrl = <Text style={styles.error}>
                  {this.state.message}
              </Text>;
          }
          ///// Check login type
          if(this.state.barcode){
            return(
              <View style={styles.container}>
              <Image style={styles.logo} source={require('image!logo')} />
                  <Text style={styles.heading}>
                    Please use QR-Scanner to login,{'\n'}
                    or enter the Login code manually.
                  </Text>
              <TouchableOpacity
                    onPress={this.onQrPressed.bind(this)}
                    style={styles.button}>
                    <Text style={styles.buttonText}>Use QR-Scanner</Text>
                </TouchableOpacity>
                <TouchableOpacity
                      onPress={this.toManuall.bind(this)}
                      >
                      <Text style={styles.change}>
                  Want to enter code manually?
                  </Text>
                </TouchableOpacity>
                {errorCtrl}

                <ActivityIndicatorIOS
                    animating={this.state.showProgress}
                    size="large"
                    style={styles.loader}
                    />
            </View>
            );
          } else {
            return(
              <View style={styles.container}>
              <Image style={styles.logo} source={require('image!logo')} />
                  <Text style={styles.heading}>
                    Please use QR-Scanner to login,{'\n'}
                    or enter the Login code manually.
                  </Text>
                  <TextInput onChangeText={(text)=> this.setState({logincode: text})} style={styles.loginInput} placeholder={this.state.logincode}>
                  </TextInput>
                    <TouchableOpacity onPress={this.onLoginPressed.bind(this)} style={styles.button} >
                    <Text style={styles.buttonText}>Log in</Text>
                    </TouchableOpacity>
                    <TouchableOpacity
                          onPress={this.toBarcode.bind(this)}
                          >
                          <Text style={styles.change}>
                      Want to use Barcode?
                      </Text>
                    </TouchableOpacity>
                    {errorCtrl}

                    <ActivityIndicatorIOS
                        animating={this.state.showProgress}
                        size="large"
                        style={styles.loader}
                        />
                </View>
            );
          }
          /////
        }
    }

    onLoginPressed(){
      if(this.state.isConnected){
        /// do the validation
        var valid = false;
        if(this.state.logincode != undefined && this.state.logincode.includes('opencampus://') && this.state.logincode.includes('access_token=') && this.state.logincode.includes('refresh_token=') && this.state.logincode.includes('id=') && this.state.logincode.includes('name=') && this.state.logincode.includes('scheme=')){
          var valid = true;
        }
        if(valid){
          console.log('Login.ios: Attempting to log in with logincode ' + this.state.logincode);
          this.setState({showProgress: true});
          console.log('Login.ios: calling AuthService class');
          var AuthService = require('./AuthService');
          AuthService.login({
              logincode: this.state.logincode
          }, (results)=> {
              this.setState(Object.assign({
                  showProgress: false
              }, results));
          console.log('Login.ios: AuthService execution finished.', results);
              if(results.success && this.props.onLogin){
                  this.props.onLogin(results);
              }
          });
        } else {
          AlertIOS.alert(
            'Invalid Input',
            'Login code you entered is not valid. Be sure to paste the whole string starting with opencampus://'
          );
        }
      } else {
        AlertIOS.alert(
          'No Connection',
          'Please check your internet connection.'
        );
      }
    }

    onQrPressed(){
      this.setState({
        showCamera: true,
      });
    }
    toManuall(){
      this.setState({
        barcode: false,
      });
    }
    toBarcode(){
      this.setState({
        barcode: true,
      });
    }
}

var styles = StyleSheet.create({
    container: {
        backgroundColor: '#00a2dd',
        paddingTop: 40,
        padding: 10,
        alignItems: 'center',
        flex: 1,
        justifyContent: 'center'
    },
    logo: {
      width: 141,
      height: 137,
    },
    heading: {
        fontSize: 18,
        margin: 10,
        marginBottom: 20,
        color: '#FFFFFF',
        paddingTop: 50,
    },
    change: {
        fontSize: 12,
        color: '#FFFFFF',
        marginTop:10,
    },
    loginInput: {
        height: 50,
        marginTop: 10,
        padding: 4,
        fontSize: 18,
        borderWidth: 1,
        borderColor: '#FFFFFF',
        borderRadius: 0,
        color: '#FFFFFF'
    },
    button: {
        height: 50,
        backgroundColor: '#48BBEC',
        borderColor: '#48BBEC',
        alignSelf: 'stretch',
        marginTop: 10,
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 5
    },
    buttonText: {
        color: '#fff',
        fontSize: 24
    },
    loader: {
        marginTop: 20
    },
    error: {
        color: 'red',
        paddingTop: 10
    }
});

module.exports = Login;

Here is AuthService.js :

'use strict';

import React, { Component } from 'react';
var SQLite = require('react-native-sqlite-storage');
var DeviceInfo = require('react-native-device-info');

class AuthService extends Component {
  constructor(props) {
        super(props);
        this.state = {
            showProgress: false
        }
        this.errorCB = this.errorCB.bind(this);
        this.successCB = this.successCB.bind(this);
    }

  errorCB(err) {
        console.log("Auth Service: error: ", err);
        this.state.progress.push("Error: " + (err.message || err));
        return false;
  }
  successCB() {
  }

  login(creds, cb){

    var db = SQLite.openDatabase({name : "oc.db", location: 'default'}, this.successCB.bind(this), this.errorCB.bind(this));
    var sql = 'CREATE TABLE IF NOT EXISTS users ('
  + 'access_token text NOT NULL,'
  + 'refresh_token text NOT NULL,'
  + 'userName text NOT NULL,'
  + 'userId text NOT NULL,'
  + 'userMail text NOT NULL,'
  + 'userSignature text NOT NULL,'
  + 'userSignatureFormat text NOT NULL,'
  + 'userCreated text NOT NULL,'
  + 'userAccess text NOT NULL,'
  + 'userLogin text NOT NULL,'
  + 'userStatus text NOT NULL,'
  + 'userTimezone text NOT NULL,'
  + 'userLanguage text NOT NULL,'
  + 'userRoles text NOT NULL,'
  + 'deviceId text NOT NULL,'
  + 'deviceName text NOT NULL,'
  + 'host text NOT NULL,'
  + 'active text NOT NULL'
+ ');';
    db.executeSql(sql, [],
            this.successCB.bind(this),
            this.errorCB.bind(this)
            );

    var LCode = creds.logincode;
    var codeSplited = LCode.split("://");
    var codeSplited2 = codeSplited[1].split("?");
    var appName = codeSplited[0];
    var serverName = codeSplited2[0];
    var splitedVars = codeSplited2[1].split("&");
    var access_token = splitedVars[0].split("=");
    var access_token = access_token[1];
    var refresh_token = splitedVars[1].split("=");
    var refresh_token = refresh_token[1];
    var uid = splitedVars[2].split("=");
    var uid = uid[1];
    var uname = splitedVars[3].split("=");
    var uname = uname[1];
    var scheme = splitedVars[4].split("=");
    var scheme = scheme[1];
    var device_id = DeviceInfo.getUniqueID();
    var device_name = DeviceInfo.getDeviceName();
    var locale = DeviceInfo.getDeviceLocale();
    console.log('AuthService: Try to fetch from : ', serverName);
    console.log('request body: ', JSON.stringify({
      uid: uid,
      refresh_token: refresh_token,
      token: access_token,
      device: device_id,
      device_name: device_name,
    }));
    fetch(scheme + '://' + serverName, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'language': locale,
        'Authorization': 'Bearer ' + access_token,
      },
      body: JSON.stringify({
        uid: uid,
        refresh_token: refresh_token,
        token: access_token,
        device: device_id,
        device_name: device_name,
      })
    })
    .then((response)=> {
      return response;
    })
    .then((response)=> {
      return response.json();
    })
    .then((results)=> {
      console.log(results);
      if(results['result'] == 1){
        console.log('Auth Service: Login was successfull');
        // User data
        var userName = results['session']['user']['name'];
        var userId = results['session']['user']['uid'];
        var userMail = results['session']['user']['mail'];
        var userSignature = results['session']['user']['signature'];
        var userSignatureFormat = results['session']['user']['signature_format'];
        var userCreated = results['session']['user']['created'];
        var userAccess = results['session']['user']['access'];
        var userLogin = results['session']['user']['login'];
        var userStatus = results['session']['user']['status'];
        var userTimezone = results['session']['user']['timezone'];
        var userLanguage = results['session']['user']['language'];
        var userRoles = results['session']['user']['roles']['2'];
        var host = results['session']['user']['host'];
        var active = 'yes';
        //var userPicture = results['session']['user']['picture'];
        console.log('Auth Service: Lets save user data to database');

        var query = "INSERT INTO users (access_token, refresh_token, userName, userId, userMail, userSignature, userSignatureFormat, userCreated, userAccess, userLogin, userStatus, userTimezone, userLanguage, userRoles, deviceId, deviceName, host, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
        var params = [access_token, refresh_token, userName,userId,userMail,userSignature,userSignatureFormat,userCreated,userAccess,userLogin,userStatus,userTimezone,userLanguage,userRoles,device_id,device_name,host,active];
        db.executeSql(query,params,
                this.successCB.bind(this),
                this.errorCB.bind(this)
                );
        return cb({
          success: true,
          userData: results['session']['user']
        });
      } else if(results['result'] == 0){
        console.log('Auth Service: Login failed message is ' + results['message']);
        return cb({
          success: false,
          message: results['message']
        });
      } else {
        console.log('Auth Service: Login failed error is ' + results['error_description']);
        return cb({
          success: false,
          message: results['error_description']
        });
      }

    })
    .catch((err)=> {
      console.log('AuthService: ' + err);
      return cb(err);
    })
    .done();

  }
}

module.exports = new AuthService();

And here is Index.js :

"use strict";

import React, {Component, PropTypes} from 'react';
import {
  AppRegistry,
  NavigatorIOS,
  StyleSheet,
  TabBarIOS,
  View,
  Text,
  StatusBar,
} from 'react-native';

var CourseList = require("./app/CourseList");
var Profile = require("./app/Profile");
import Icon from 'react-native-vector-icons/Ionicons';
var SQLite = require('react-native-sqlite-storage');
var Login = require("./app/Login");
var db = SQLite.openDatabase({name : "oc.db", location: 'default'});

StatusBar.setBarStyle('light-content');

class OpenCampus extends Component {
  constructor(props) {
        super(props);
        this.state = {
          selectedTab: "Courses",
          isLoggedIn: false,
          userId: null,
        };
    }

  componentWillMount(){
    var query = "SELECT * FROM users WHERE active='yes'";
      var params = [];
      db.transaction((tx) => {
            tx.executeSql(query,params, (tx, results) => {
                var len = results.rows.length;
                if(len > 0){
                  let row = results.rows.item(0);
                  this.setState({
                    isLoggedIn: true,
                    userId: row.userId
                  });
                }
              }, function(){
                console.log('index: Something went wrong');
              });
          });
    }

  onLogin(results) {
    this.setState({
      isLoggedIn: true,
    });
  }

  logout() {
    console.log("Logout called from index");
    var query = "DELETE FROM users WHERE userId=?";
    var params = [this.state.userId];
    db.transaction((tx) => {
          tx.executeSql(query,params, (tx, results) => {
            ///// check if there is other accounts on database, if yes, make first row active
            var query = "SELECT * FROM users WHERE active='yes'";
              var params = [];
              db.transaction((tx) => {
                    tx.executeSql(query,params, (tx, results) => {
                        var len = results.rows.length;
                        if(len > 0){
                          let row = results.rows.item(0);
                          userId = row.userId;
                          ///// Set new user active
                          var query = "UPDATE users SET active='yes' WHERE userId=?";
                            var params = [userId];
                            db.transaction((tx) => {
                                  tx.executeSql(query,params, (tx, results) => {
                                      console.log('index: Active Account Changed');
                                    }, function(){
                                      console.log('index: Something went wrong');
                                    });
                                });
                          ///////
                          this.setState({
                              isLoggedIn: true,
                              userId: userId,
                            });
                        } else {
                          this.setState({
                            isLoggedIn: false,
                            userId: null,
                          });
                        }
                      }, function(){
                        console.log('index: Something went wrong');
                      });
                  });
            /////
            }, function(){
              console.log('index: Something went wrong when logging out');
            });
        });
  }


  _renderCourses() {
    return (
      <NavigatorIOS style={styles.wrapper}
        barTintColor='#00a2dd'
        titleTextColor='#fff'
        tintColor='#ffffff'
        ref='RCourses'
        initialRoute={{
          component: CourseList,
          title: 'Courses',
          passProps: {filter: 'Courses'},
        }}
      />
    );
  }
  _renderRegister() {
    return (
      <NavigatorIOS style={styles.wrapper}
        barTintColor='#00a2dd'
        titleTextColor='#fff'
        tintColor='#ffffff'
        ref='RRegister'
        initialRoute={{
          component: CourseList,
          title: 'Register',
          passProps: {filter: 'Register'},
        }}
      />
    );
  }
  _renderProfile() {
    return (
      <NavigatorIOS style={styles.wrapper}
        barTintColor='#00a2dd'
        titleTextColor='#fff'
        tintColor='#ffffff'
        ref='RProfile'
        initialRoute={{
          component: Profile,
          title: 'Profile',
          passProps: {filter: 'Profile'},
          rightButtonTitle: 'Logout',
          onRightButtonPress: () => this.logout(),
          leftButtonTitle: 'Add Account',
          onLeftButtonPress: () => this.addnew(),
        }}
      />
    );
  }


  addnew() {
    console.log('Send user to login page to add new account');
    //// Set old user to inactive
    var query = "UPDATE users SET active='no' WHERE active='yes'";
      var params = [this.state.userId];
      db.transaction((tx) => {
            tx.executeSql(query,params, (tx, results) => {
              //// Set login status to false so login screen will be shown
              console.log(results);
              this.setState({
                isLoggedIn: false,
                userId: null,
              });
              }, function(){
                console.log('index: Something went wrong when adding new account');
              });
          });
  }


  popAll(){
    if(typeof this.refs.RCourses !== typeof undefined){
      this.refs.RCourses.popToTop();
    }
    if(typeof this.refs.RRegister !== typeof undefined){
      this.refs.RRegister.popToTop();
    }
    if(typeof this.refs.RProfile !== typeof undefined){
      this.refs.RProfile.popToTop();
    }
  }

  render() {
    if(!this.state.isLoggedIn){
      console.log('index: User not logged in. redirecting to Login page.');
      return(
        <Login onLogin={this.onLogin.bind(this)} />
      );
    } else {
      console.log('index: User is logged in lets show the content');
    return (
      <TabBarIOS tintColor={"#00a2dd"}>
        <Icon.TabBarItem
          title="Courses"
          iconName="ios-list-outline"
          selectedIconName="ios-list-outline"
          selected={this.state.selectedTab === "Courses"}
          onPress={() => {
            this.setState({
              selectedTab: "Courses",
            });
            this.popAll();
          }}>
          {this._renderCourses()}
        </Icon.TabBarItem>
        <Icon.TabBarItem
          title="Register"
          iconName="ios-book"
          selectedIconName="ios-book"
          selected={this.state.selectedTab === "Register"}
          onPress={() => {
            this.setState({
              selectedTab: "Register",
            });
            this.popAll();
          }}>
          {this._renderRegister()}
        </Icon.TabBarItem>
        <Icon.TabBarItem
          title="Profile"
          iconName="ios-person"
          selectedIconName="ios-person"
          selected={this.state.selectedTab === "Profile"}
          onPress={() => {
            this.setState({
              selectedTab: "Profile",
            });
            this.popAll();
          }}>
          {this._renderProfile()}
        </Icon.TabBarItem>
      </TabBarIOS>
    );
  }
  }
}

var styles = StyleSheet.create({
  tabContent: {
    flex: 1,
    alignItems: "center",
  },
  tabText: {
    color: "white",
    margin: 50,
  },
  wrapper: {
    flex: 1,
    backgroundColor: '#00a2dd',
  }
});

AppRegistry.registerComponent('OpenCampus', () => OpenCampus);

UPDATE : here is the crash log by apple : http://www.ataomega.com/temp..suczkfac.crash http://www.ataomega.com/temp..hsbgdlod.crash

Alban answered 5/8, 2016 at 7:21 Comment(6)
The Apple note says that your app crashed when they tapped "login" while offline and when on WiFi; these are two separate test cases, not offline & wifi at the same time as that would make no sense :). Can you reproduce this fault?Ensepulcher
Not unfortunately. app never crashes on my tests :( here is the crash log : ataomega.com/temp..suczkfac.crashAlban
There is no meaningful backtrace in that crash dump, so all I can say is that some code called "abort()". I have no idea what debugging/crash analysis tools are available for javascript.Ensepulcher
Thanks for your reply @EnsepulcherAlban
@Ataomega did you find the solution for this problem and can I know which react native version you have used ,Your crash Logs have been removed.Moshe
@IsmailIqbal problem was not from react-native , fetch function actually supports ipv6. our server wasn't listening on ipv6Alban
R
6

You should test your app for ipv6 compatibility. Here is a tutorial that explains how to do that.

  • Boot OS X 10.11
  • Make sure your Mac is connected to the Internet, but not through Wi-Fi.
  • Launch System Preferences from your Dock, LaunchPad, or the Apple menu.
  • Press the Option key and click Sharing. Don’t release the Option key yet.
  • Select Internet Sharing in the list of sharing services.
  • Release the Option key.
  • Select the Create NAT64 Network checkbox.
  • Choose the network interface that provides your Internet connection, such as Thunderbolt Ethernet.
  • Select the Wi-Fi checkbox.
  • Click Wi-Fi Options, and configure the network name and security options for your network.
  • Setting up local Wi-Fi network options
  • Select the Internet Sharing checkbox to enable your local network.
  • When prompted to confirm you want to begin sharing, click Start.
  • Once sharing is active, you should see a green status light and a label that says Internet Sharing: On. In the Wi-Fi menu, you will also see a small, faint arrow pointing up, indicating that Internet Sharing is enabled. You now have an IPv6 NAT64 network and can connect to it from other devices in order to test your app.
Ribble answered 5/8, 2016 at 7:30 Comment(2)
Links to external resources are encouraged, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline.Ensepulcher
Thanks for your answer. i've already did that. but when i do, mac itself can access ipv6 sites like ipv6.google.com but the iphone connected to mac, cannot access ipv6 websitesAlban

© 2022 - 2024 — McMap. All rights reserved.