Skip to content

Contribution Guide (Internal)

Darren Chan edited this page Sep 15, 2023 · 7 revisions

TL;DR

  1. Setup: Use an outer repo for testing and an inner repo for development.
  2. Coding: Use the TypeScript (not JavaScript) files.
  3. Testing: Run npm start in the outer repo's example folder and proceed with testing.
  4. Making a New Branch and Pull Request: Create PRs to master branch. Every time you update your PR, GitHub Actions will update JS files and commit its changes to the PR. Every PR must have 1 approval before merge (unless you are an administrator).
  5. Upgrading: upgrade example app by following the react-native documentation for upgrades.

Setup

  1. Clone the repo. This is your "outer repo" and is used for testing the app.
  2. In example folder, run npm install. For now, if you get an error like "unable to resolve dependency tree", switch to npm version 6.14.15.
  3. In example/node_modules/, delete react-native-pdftron.
  4. In example/node_modules/, clone the repo a second time and rename it to react-native-pdftron. This is the "inner repo", used for development.
  5. In example/node_modules/react-native-pdftron, run npm install.
  6. Run npm install -g typescript to install the TypeScript compiler globally.
  7. If the testing environment fails - instead of running npm install, run yarn install on all those above steps

Coding

In order to provide TypeScript support for users, the JavaScript files have been migrated to TypeScript. The files now use .ts or .tsx file extension. For example, DocumentView.js became DocumentView.tsx.

You may notice red squiggly lines and warnings/errors while coding. These are TypeScript type checking errors, not syntax errors. Correct them if you can; otherwise, ignore them by commenting // @ts-ignore.

Step-by-step:

Adding Config constants

  1. Open the src/Config/Config.ts file.
  2. Add your constant(s) to the Config object.
  3. If you created a whole new Config category (like Buttons or Tools), scroll down to the Config module and add export type NewCategory = ValueOf<typeof Config.NewCategory>;

Adding an AnnotOptions type or interface

AnnotOptions is used to store reusable object types. These types can be useful in DocumentView methods and event listeners.

  1. Open the src/AnnotOptions/AnnotOptions.ts file.
  2. Add a new type or interface to represent the object.

Adding a DocumentView prop (excluding event listeners)

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. Add your prop to the propTypes object.
    • If the prop type is “a Config.* constant”, use the oneOf<>() helper method.
    • If the prop type is “an array of Config.* constants”, use the arrayOf<>() helper method.
    • Otherwise, use the standard PropTypes types.

Adding a DocumentView event listener

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. Add your event listener to the propTypes object and use the func<>() helper method.
    • To represent objects that may show up more than once, like annotations or rects, use AnnotOptions.
  3. Add an if/else case for your event listener to DocumentView.onChange method.

Adding a DocumentView method

  1. Open the src/DocumentView/DocumentView.tsx file.
  2. Add your method to the DocumentView class.
    • The return type should always look like Promise<void | T> where T is the expected return type upon success. If your method returns void, use Promise<void>.

Adding an RNPdftron method

  1. Open the index.ts file.
  2. Add the method to the Pdftron interface.

Editing PDFViewCtrl

  1. Open the src/PDFViewCtrl/PDFViewCtrl.tsx file.
  2. Follow the same steps used for elements in DocumentView.

Testing

  1. First, generate JavaScript files from the TypeScript ones:
    • In the outer repo's example folder, run npm start, npm run android, or npm run ios.
    • OR go to the inner repo and run tsc (or npx tsc if you have not installed tsc globally).
  2. Proceed with testing in App.js.
    • If you would like to observe the effects of TypeScript support from the user's perspective, see Usage Example.

Making a New Branch and Pull Request

  1. Create a branch from master branch.
  2. Update the source code (in *.ts files).
  3. Commit and push the changes with descriptive messages.
  4. Create a pull request to master branch.

Note:

  • Every time you open/push to/reopen a PR, GitHub Actions will automatically update JS files and push the update to your PR. These changes will not affect the source code (TS files).
  • Every PR needs 1 approving review to be merged into master (unless you are an administrator of the repo).
  • In the rare case that you want to create a PR from your fork to this original repo, the script will not succeed for safety reasons. Instead, the JS files will have to be updated manually. Alternatively, you could transfer your changes to a branch on the original repo and create the PR from here.

Other

Q: What if I created a branch from master before the TypeScript update? Will there be lots of conflicts if I update from master after the TypeScript update?

A: It is unlikely that you will face lots of conflicts. There may be a few in the DocumentView file because some of its structure has changed. The most likely conflict is because the propTypes object has been moved from inside DocumentView class to outside. If you encounter this conflict, make note of what prop you have added, accept the move, and re-add the prop after the move.

If the conflicts are more severe, create a new branch from master and copy your changes to it.

Usage Example

You can copy the code snippet below into an App.tsx to test the tooling for the new library. Note that this file cannot be used to build an app unless a TypeScript project is created:

import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View,
  PermissionsAndroid,
  BackHandler,
  NativeModules,
  Alert,
  Button
} from 'react-native';

import { AnnotOptions, Config, DocumentView, RNPdftron} from 'react-native-pdftron';

type Props = {};
type State = {permissionGranted: boolean};
let _viewer : DocumentView | null;

export default class App extends Component<Props, State> {

  constructor(props: Props) {
    super(props);

    // Uses the platform to determine if storage permisions have been automatically granted.
    // The result of this check is placed in the component's state.
    this.state = {
      permissionGranted: Platform.OS === 'ios' ? true : false
    };

    RNPdftron.initialize("Insert commercial license key here after purchase");
    RNPdftron.enableJavaScript(true);
  }

  // Uses the platform to determine if storage permissions need to be requested.
  componentDidMount() {
    if (Platform.OS === 'android') {
      this.requestStoragePermission();
    }
  }

  // Requests storage permissions for Android and updates the component's state using 
  // the result.
  async requestStoragePermission() {
    try {
      const granted = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
      );
      if (granted === PermissionsAndroid.RESULTS.GRANTED) {
        this.setState({
          permissionGranted: true
        });
        console.log("Storage permission granted");
      } else {
        this.setState({
          permissionGranted: false
        });
        console.log("Storage permission denied");
      }
    } catch (err) {
      console.warn(err);
    }
  }

  onLeadingNavButtonPressed = () => {
    if (_viewer != null) {
      _viewer.getPageNumberFromScreenPoint(0, 5894).then((page : number | void) => {
        if (typeof page === "number") {
          console.log("Page associated with coordinates: ", page);
        }
      });
    }
  }

  onDocumentLoaded = (path: string) => {
    if (_viewer != null) {
      _viewer.importAnnotationCommand( 
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
      "    <xfdf xmlns=\"http://ns.adobe.com/xfdf/\" xml:space=\"preserve\">\n" +
      "      <add>\n" +
      "        <square style=\"solid\" width=\"5\" color=\"#E44234\" opacity=\"1\" creationdate=\"D:20200619203211Z\" flags=\"print\" date=\"D:20200619203211Z\" name=\"c684da06-12d2-4ccd-9361-0a1bf2e089e3\" page=\"1\" rect=\"113.312,277.056,235.43,350.173\" title=\"\" />\n" +
      "      </add>\n" +
      "      <modify />\n" +
      "      <delete />\n" +
      "      <pdf-info import-version=\"3\" version=\"2\" xmlns=\"http://www.pdftron.com/pdfinfo\" />\n" +
      "    </xfdf>"
      ).catch((e) => console.warn(e));
    }
  }

  onAnnotationMenuPress = (event: {annotationMenu:string, annotations: Array<AnnotOptions.Annotation>}) => {
    console.log("Menu item pressed: ", event.annotationMenu);
    event.annotations.forEach((annot) => {
      console.log("Annotation changed with Annotation Menu: ");
      console.log("Rect:\n", annot.pageRect, "\n", annot.screenRect);
    });
  }

  onAnnotationSelected = (event: {annotations: Array<AnnotOptions.Annotation>}) => {
    event.annotations.forEach((annot) => {
      console.log("Selected Annotation:\n");

      if (_viewer != null && typeof annot.screenRect === "object") {
        _viewer.getAnnotationListAt(annot.screenRect.x1, annot.screenRect.y1, annot.screenRect.x2, annot.screenRect.y2)
        .then((annotations) => {
          if (Array.isArray(annotations)) {
            annotations.forEach((annotation: AnnotOptions.Annotation) => {
              console.log("Annotation from list: ",annotation);
            })
          }
        });

        _viewer.getAnnotationAtPoint(annot.screenRect.x1, annot.screenRect.y1, 57, 12)
        .then((anno) => {
          if (typeof anno === "object") {
            console.log("Annotation at Point: ", anno);
          }
        });

      }
    });
  }

  onAnnotationChanged = (event: {action: string, annotations: Array<AnnotOptions.Annotation>}) => {
    event.annotations.forEach((annot) => {
      console.log("Changed Annotation: ", annot);
      console.log("Action: ", event.action);
    });
  }

  onExportAnnotationCommand = (event: {action: string, xfdfCommand: string, annotations: Array<AnnotOptions.Annotation>}) => {
    event.annotations.forEach((annot) => {
      console.log("Exported Annotation: ", annot);
      console.log("Action: ", event.action);
    })
  }

  onBehaviourActivated = (event: {action: Config.Actions, data: AnnotOptions.LinkPressData | AnnotOptions.StickyNoteData}) => {
    console.log("Action is ", event.action);
    console.log("Data is ", event.data);
    
  }

  onPageChanged = (event: {previousPageNumber: number, pageNumber: number}) => {
    console.log('Previous page:', event.previousPageNumber, 'current page:', event.pageNumber);
  }

  onToolChanged = (event: {previousTool: Config.Tools | "unknown tool", tool: Config.Tools | "unknown tool"}) => {
    console.log('Previous tool: ', event.previousTool, " current tool: ", event.tool);
  }

  // A callback for when button 1 is pressed
  buttonFunction1 = async () => {
    if (_viewer != null) {
      let ret = await _viewer.gotoPreviousPage();
      console.log("Return value: ", ret);
    }
  }

  // A callback for when button 2 is pressed
  buttonFunction2 = async () => {
    if (_viewer != null) {
      let ret = await _viewer.gotoNextPage();
      console.log("Return value: ", ret);
    }
  }

  render() {
    // If the component's state indicates that storage permissions have not been granted, 
    // a view is loaded prompting users to grant these permissions.
    if (!this.state.permissionGranted) {
      return (
        <View style={styles.container}>
          <Text>
            Storage permission required.
          </Text>
        </View>
      )
    }

    const path = "https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf";

    const toolbar = {
      [Config.CustomToolbarKey.Id]: 'toolbar',
      [Config.CustomToolbarKey.Name]: 'Custom Toolbar',
      [Config.CustomToolbarKey.Icon] : Config.ToolbarIcons.Draw,
      [Config.CustomToolbarKey.Items]: [
        Config.Tools.annotationCreateFreeHand,
        Config.Tools.annotationCreateArrow,
        Config.Tools.annotationCreateFreeText,
        Config.Tools.annotationCreateLine,
        Config.Tools.annotationEraserTool,
        Config.Buttons.undo,
        Config.Buttons.redo
      ],
    };

    return (
      <View style={styles.container}>
        <DocumentView
          ref={(ref) => _viewer = ref}
          document={path}
          showLeadingNavButton={true}
          onLeadingNavButtonPressed={this.onLeadingNavButtonPressed}
          onDocumentLoaded={this.onDocumentLoaded}
          // overrideAnnotationMenuBehavior={[Config.AnnotationMenu.flatten]}
          // onAnnotationMenuPress={this.onAnnotationMenuPress}
          onAnnotationChanged={this.onAnnotationChanged}
          onAnnotationsSelected={this.onAnnotationSelected}
          onExportAnnotationCommand={this.onExportAnnotationCommand}
          // overrideBehavior={[Config.Actions.linkPress, Config.Actions.stickyNoteShowPopUp]}
          // onBehaviorActivated={this.onBehaviourActivated}
          // onPageChanged={this.onPageChanged}
          // onToolChanged={this.onToolChanged}
          disabledElements={[Config.Buttons.saveCopyButton]}
          annotationToolbars={[toolbar]}
        />
        <View style={styles.buttonView}>
          <Button title="1" onPress={this.buttonFunction1}/>
          <Button title="2" onPress={this.buttonFunction2}/>
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  buttonView : {
    flexDirection: 'row',
    height: 50,
    justifyContent: 'space-between'
  }
});

Upgrading

Please follow this guide to upgrade example app to the latest version of React Native