npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@fancode/react-native-codepush-joystick

v0.0.2

Published

A flexible CodePush Joystick for React Native apps

Readme

React Native CodePush Joystick

A flexible and powerful CodePush management library for React Native applications that integrates seamlessly with GitHub and CI/CD pipelines. This library provides a comprehensive solution for managing CodePush updates, building apps from pull requests, and providing developers with a convenient "joystick" interface to test different versions of their app.

Features

  • 🚀 CodePush Integration: Seamless integration with Microsoft CodePush for over-the-air updates
  • 🔧 GitHub Integration: Fetch pull requests and trigger builds directly from GitHub
  • ⚙️ CI/CD Support: Built-in support for GitHub Actions and custom CI/CD providers
  • 📱 React Native Hook: Easy-to-use React hook for state management
  • 🔄 Real-time Updates: Monitor build status and download progress in real-time
  • 🎯 Flexible Versioning: Support for PR-based and custom versioning strategies
  • 🛠️ Build Management: Trigger, monitor, and cancel builds programmatically
  • 📊 Comprehensive Callbacks: Extensive callback system for lifecycle events

Installation

npm install @fancode/react-native-codepush-joystick
# or
yarn add @fancode/react-native-codepush-joystick

Peer Dependencies

Make sure you have the required peer dependencies installed:

npm install react react-native
# or
yarn add react react-native

Quick Start

1. Basic Setup with GitHub Actions

import React, { useEffect, useState } from "react";
import { View, Text, TouchableOpacity, FlatList } from "react-native";
import {
  useCodePushManager,
  createGitHubActionsCICDProvider,
  CodePushActionButtonState,
} from "@fancode/react-native-codepush-joystick";

export default function CodePushJoystick() {
  const [pullRequests, setPullRequests] = useState([]);

  // Configure the CodePush manager with GitHub Actions CI/CD provider
  const config = {
    sourceControl: {
      config: {
        owner: "your-github-username",
        repo: "your-repo-name",
        token: "your-github-token",
      },
    },
    cicdProvider: createGitHubActionsCICDProvider({
      owner: "your-github-username",
      repo: "your-repo-name",
      token: "your-github-token",
      workflowFile: "codepush-build.yml", // Your GitHub workflow file
      workflowInputs: {
        DEPLOYMENT_NAME: "Staging",
      },
    }),
    codepush: {
      deploymentKey: "YOUR_DEPLOYMENT_KEY", // Your CodePush deployment key (from App Center)
    },
    appVersion: "1.0.0", // Your current app version
    callbacks: {
      onPullRequestsFetched: (prs) => setPullRequests(prs),
      onError: (error, context) => console.error(`Error in ${context}:`, error),
      onCodePushAvailable: (pr, packageInfo) => {
        console.log(`CodePush available for PR #${pr.number}`);
      },
      onDownloadStarted: (pullRequest, packageInfo) => {
        console.log(
          `Starting download for PR #${pullRequest.number}: ${packageInfo.label}`
        );
      },
      onDownloadComplete: (pullRequest, localPackage) => {
        // Store version information after successful download
        const formattedVersion = manager.formatVersionForStorage(localPackage);
        AsyncStorage.setItem("app_center_version", formattedVersion);
      },
    },
  };

  const { manager, stateMap, fetchPullRequests } = useCodePushManager(config);

  useEffect(() => {
    fetchPullRequests({ state: "open", per_page: 10 });
  }, [fetchPullRequests]);

  const renderPullRequest = ({ item: pr }) => {
    const state = stateMap[pr.id] || {};
    const buttonText = CodePushActionButtonState[state.status] || "Status";

    return (
      <View style={{ padding: 16, borderBottomWidth: 1 }}>
        <Text style={{ fontSize: 16, fontWeight: "bold" }}>
          #{pr.number} - {pr.title}
        </Text>
        <Text style={{ color: "#666" }}>
          Branch: {pr.head?.ref} | Author: {pr.user.login}
        </Text>

        {state.message && (
          <Text style={{ color: "#888", marginTop: 4 }}>{state.message}</Text>
        )}

        {state.progress !== null && (
          <Text style={{ color: "#007AFF" }}>
            Download Progress: {state.progress}%
          </Text>
        )}

        <TouchableOpacity
          style={{
            backgroundColor: state.loading ? "#ccc" : "#007AFF",
            padding: 12,
            borderRadius: 8,
            marginTop: 8,
          }}
          disabled={state.loading}
          onPress={() => manager?.processWorkflow(pr)}
        >
          <Text style={{ color: "white", textAlign: "center" }}>
            {state.loading ? "Loading..." : buttonText}
          </Text>
        </TouchableOpacity>
      </View>
    );
  };

  return (
    <View style={{ flex: 1 }}>
      <Text style={{ fontSize: 20, fontWeight: "bold", padding: 16 }}>
        CodePush Joystick
      </Text>
      <FlatList
        data={pullRequests}
        keyExtractor={(item) => item.id.toString()}
        renderItem={renderPullRequest}
      />
    </View>
  );
}

2. Custom CI/CD Provider Setup

import { createCustomCICDProvider } from "@fancode/react-native-codepush-joystick";

const customProvider = createCustomCICDProvider({
  triggerBuild: async (params) => {
    // Your custom build trigger logic
    const response = await fetch("https://your-ci-cd-api.com/trigger", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        branch: params.branch,
        version: params.version,
      }),
    });

    const data = await response.json();
    return {
      buildId: data.buildId,
      status: {
        status: data.status,
        conclusion: data.conclusion,
      },
      startedAt: data.startedAt,
    };
  },

  getWorkflowRuns: async (branchName) => {
    // Fetch workflow runs for the branch
    const response = await fetch(
      `https://your-ci-cd-api.com/runs?branch=${branchName}`
    );
    return response.json();
  },

  cancelBuild: async (buildId) => {
    // Cancel a specific build
    await fetch(`https://your-ci-cd-api.com/builds/${buildId}/cancel`, {
      method: "POST",
    });
  },

  findWorkflowStatus: (workflowRun) => {
    if (!workflowRun) return null;

    return {
      workflowStatus: {
        isRunning: workflowRun.status === "running",
        isFailed: workflowRun.status === "failed",
        isCancelled: workflowRun.status === "cancelled",
        isCompleted: workflowRun.status === "completed",
        rawStatus: workflowRun.status,
        id: workflowRun.id,
        startedAt: new Date(workflowRun.startedAt),
      },
      buildInfo: {
        buildId: workflowRun.id,
        status: {
          status: workflowRun.status,
        },
        startedAt: workflowRun.startedAt,
      },
    };
  },
});

// Use the custom provider in your config
const config = {
  // ... other config
  cicdProvider: customProvider,
};

Simple Usage Example

import {
  useCodePushManager,
  createGitHubActionsCICDProvider,
} from "@fancode/react-native-codepush-joystick";

function MyComponent() {
  const { manager, stateMap, fetchPullRequests, resetState } =
    useCodePushManager({
      sourceControl: {
        config: { owner: "user", repo: "repo", token: "token" },
      },
      cicdProvider: createGitHubActionsCICDProvider({
        /* config */
      }),
      codepush: { deploymentKey: "key" },
      appVersion: "1.0.0",
    });

  useEffect(() => {
    fetchPullRequests({ state: "open", per_page: 10 });
  }, [fetchPullRequests]);

  return (
    <FlatList
      data={pullRequests}
      renderItem={({ item }) => {
        const state = stateMap[item.id];
        return (
          <View>
            <TouchableOpacity onPress={() => handleAction(item)}>
              <Text>
                {item.title} - {state?.status}
              </Text>
            </TouchableOpacity>
            {state?.status === "ERROR" && (
              <TouchableOpacity onPress={() => resetState(item.id)}>
                <Text>Reset</Text>
              </TouchableOpacity>
            )}
          </View>
        );
      }}
    />
  );
}

Configuration

CI/CD Provider Configuration

The library supports two types of CI/CD providers. You must choose one and configure it properly:

1. GitHub Actions CI/CD Provider

Use createGitHubActionsCICDProvider when your builds are handled by GitHub Actions:

import { createGitHubActionsCICDProvider } from "@fancode/react-native-codepush-joystick";

const githubActionsProvider = createGitHubActionsCICDProvider({
  owner: "your-github-username",
  repo: "your-repo-name",
  token: "your-github-token", // GitHub personal access token
  workflowFile: "codepush-build.yml", // Your workflow file name
  workflowInputs: {
    // Optional: additional inputs for your workflow
    DEPLOYMENT_NAME: "Staging",
    BUILD_TYPE: "release",
  },
});

Required Permissions for GitHub Token:

  • repo (full control of private repositories)
  • workflow (update GitHub Action workflows)

2. Custom CI/CD Provider

Use createCustomCICDProvider when using other CI/CD services (Jenkins, Azure DevOps, CircleCI, etc.):

import { createCustomCICDProvider } from "@fancode/react-native-codepush-joystick";

const customCICDProvider = createCustomCICDProvider({
  // Trigger a new build
  triggerBuild: async (params) => {
    const response = await fetch("https://your-ci-api.com/trigger", {
      method: "POST",
      headers: {
        Authorization: "Bearer YOUR_CI_TOKEN",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        branch: params.branch,
        version: params.version,
        // Add your CI-specific parameters
      }),
    });

    const data = await response.json();
    return {
      buildId: data.buildId,
      status: {
        status: data.status,
        conclusion: data.conclusion,
      },
      startedAt: data.startedAt,
    };
  },

  // Get workflow runs for a branch
  getWorkflowRuns: async (branchName) => {
    const response = await fetch(
      `https://your-ci-api.com/runs?branch=${branchName}`
    );
    const runs = await response.json();
    return runs.map((run) => ({
      id: run.id,
      status: run.status,
      startedAt: run.startedAt,
      extra: run.metadata, // Optional additional data
    }));
  },

  // Cancel a running build
  cancelBuild: async (buildId) => {
    await fetch(`https://your-ci-api.com/builds/${buildId}/cancel`, {
      method: "POST",
      headers: { Authorization: "Bearer YOUR_CI_TOKEN" },
    });
  },

  // Determine workflow status from run data
  findWorkflowStatus: (workflowRun) => {
    if (!workflowRun) return null;

    return {
      workflowStatus: {
        isRunning: workflowRun.status === "running",
        isFailed: workflowRun.status === "failed",
        isCancelled: workflowRun.status === "cancelled",
        isCompleted: workflowRun.status === "completed",
        rawStatus: workflowRun.status,
        id: workflowRun.id,
        startedAt: new Date(workflowRun.startedAt),
      },
      buildInfo: {
        buildId: workflowRun.id,
        status: {
          status: workflowRun.status,
        },
        startedAt: workflowRun.startedAt,
      },
    };
  },
});

CodePushManagerConfig

The main configuration object for the CodePush manager:

interface CodePushManagerConfig {
  sourceControl: {
    config: GitHubConfig;
  };
  cicdProvider: CICDProvider; // Use either createGitHubActionsCICDProvider or createCustomCICDProvider
  codepush: {
    deploymentKey?: string;
  };
  appVersion: string;
  versioning?: {
    strategy?: "pr-based" | "custom";
    customCalculator?: (pr: GithubPullRequest, baseVersion: string) => string;
  };
  callbacks?: CodePushCallbacks;
}

Complete Configuration Examples

Using GitHub Actions Provider:

import {
  useCodePushManager,
  createGitHubActionsCICDProvider,
} from "@fancode/react-native-codepush-joystick";

const config = {
  sourceControl: {
    config: {
      owner: "your-github-username",
      repo: "your-repo-name",
      token: "your-github-token",
    },
  },
  cicdProvider: createGitHubActionsCICDProvider({
    owner: "your-github-username",
    repo: "your-repo-name",
    token: "your-github-token",
    workflowFile: "codepush-build.yml",
    workflowInputs: {
      DEPLOYMENT_NAME: "Staging",
    },
  }),
  codepush: {
    deploymentKey: "YOUR_DEPLOYMENT_KEY",
  },
  appVersion: "1.0.0",
  callbacks: {
    onError: (error, context) => console.error(`Error in ${context}:`, error),
    onDownloadStarted: (pullRequest, packageInfo) => {
      console.log(
        `Starting download for PR #${pullRequest.number}: ${packageInfo.label}`
      );
    },
    onDownloadComplete: (pullRequest, localPackage) => {
      // Store version information after successful download
      const formattedVersion = manager.formatVersionForStorage(localPackage);
      AsyncStorage.setItem("app_center_version", formattedVersion);
    },
  },
};

const { manager, stateMap } = useCodePushManager(config);

Using Custom CI/CD Provider:

import {
  useCodePushManager,
  createCustomCICDProvider,
} from "@fancode/react-native-codepush-joystick";

const config = {
  sourceControl: {
    config: {
      owner: "your-github-username",
      repo: "your-repo-name",
      token: "your-github-token",
    },
  },
  cicdProvider: createCustomCICDProvider({
    triggerBuild: async (params) => {
      // Your custom build logic here
      return await yourCustomBuildService.trigger(params);
    },
    getWorkflowRuns: async (branchName) => {
      return await yourCustomBuildService.getRuns(branchName);
    },
    cancelBuild: async (buildId) => {
      await yourCustomBuildService.cancel(buildId);
    },
    findWorkflowStatus: (workflowRun) => {
      return yourCustomBuildService.parseStatus(workflowRun);
    },
  }),
  codepush: {
    deploymentKey: "YOUR_DEPLOYMENT_KEY",
  },
  appVersion: "1.0.0",
  callbacks: {
    onDownloadStarted: (pullRequest, packageInfo) => {
      console.log(
        `Starting download for PR #${pullRequest.number}: ${packageInfo.label}`
      );
    },
    onDownloadComplete: (pullRequest, localPackage) => {
      // Store version information after successful download
      const formattedVersion = manager.formatVersionForStorage(localPackage);
      AsyncStorage.setItem("app_center_version", formattedVersion);
    },
  },
};

const { manager, stateMap } = useCodePushManager(config);

GitHubConfig

interface GitHubConfig {
  owner: string; // GitHub repository owner
  repo: string; // Repository name
  token: string; // GitHub personal access token
}

GitHubActionsConfig

interface GitHubActionsConfig {
  owner: string;
  repo: string;
  token: string;
  workflowFile: string; // e.g., 'codepush-build.yml'
  workflowInputs?: Record<string, string>; // Additional workflow inputs
}

Custom Versioning

You can implement custom versioning strategies:

const config = {
  // ... other config
  versioning: {
    strategy: "custom",
    customCalculator: (pr, baseVersion) => {
      // Example: Use PR number and branch name for versioning
      const [major, minor, patch] = baseVersion.split(".");
      const newPatch = parseInt(patch) + pr.number;
      return `${major}.${minor}.${newPatch}-${pr.head?.ref}`;
    },
  },
};

Version Storage After Download

The library provides a callback to handle version storage after a CodePush update is downloaded. This is useful for tracking which version is currently installed:

const config = {
  // ... other config
  callbacks: {
    onDownloadComplete: (pullRequest, localPackage) => {
      // Format version similar to your existing pattern
      const formattedVersion = manager.formatVersionForStorage(localPackage);

      AsyncStorage.setItem("app_center_version", formattedVersion);
    },
    onDownloadProgress: (pullRequest, progress) => {
      console.log(`Download progress: ${progress}%`);
    },
    // ... other callbacks
  },
};

Manual Version Formatting

You can also manually format the version string:

const config = {
  // ... other config
  callbacks: {
    onDownloadComplete: (pullRequest, localPackage) => {
      // Custom version formatting
      let version = localPackage.label.replace("v", "");

      if (isInternalBuild && localPackage.description) {
        version += "/" + localPackage.description;
      }

      // Store the version using your preferred storage mechanism
      AsyncStorage.setItem("app_center_version", version);
    },
  },
};

CodePush Workflow & Lifecycle

The library provides both a convenience method for automatic workflow management and individual methods for granular control.

Automated Workflow with processWorkflow (Recommended)

The processWorkflow method automatically determines the next action based on the current state:

// Simple automated approach. Use this function in your action button
const newState = await manager.processWorkflow(pullRequest);

State-based Actions:

  • NOT_CHECKED / UN_AVAILABLE / ERROR → Check for CodePush updates
  • AVAILABLE → Download the update
  • DOWNLOADED → Restart the app
  • ALREADY_RUNNING → Cancel the running build
  • NOT_RUNNING → Trigger a new build

Manual Workflow Control

For advanced use cases, you can control each step manually:

1. Check for Updates

// Step 1: Check if CodePush update is available
const state = await manager.checkCodePushUpdate(pullRequest);

// Callbacks triggered:
// - onCodePushAvailable (if update found)
// - onStateChange

2. Handle Build Management

If no CodePush update is found, the system checks for running builds:

// This is called automatically by checkCodePushUpdate
// But you can call it manually:
await manager.checkGitHubWorkflows(pullRequest);

// Possible outcomes:
// - Build is running → Status: ALREADY_RUNNING
// - No build found → Status: NOT_RUNNING

3. Trigger New Build

// Trigger a new build for the pull request
const buildInfo = await manager.triggerBuild(pullRequest);

// Callbacks triggered:
// - onBuildTriggered
// - onStateChange

4. Download Update

// Download the CodePush update
const state = await manager.downloadCodePushUpdate(pullRequest);

// Callbacks triggered in sequence:
// - onDownloadStarted
// - onDownloadProgress (multiple times)
// - onDownloadComplete
// - onStateChange

5. Install & Restart

// Restart the app to apply the update
await manager.restartApp();

Complete Manual Workflow Example

const handleManualWorkflow = async (pullRequest) => {
  try {
    // Step 1: Check for existing CodePush update
    console.log("Checking for CodePush update...");
    let state = await manager.checkCodePushUpdate(pullRequest);

    if (state.status === CodePushStatus.AVAILABLE) {
      // Step 2: Download the update
      console.log("CodePush update available, downloading...");
      state = await manager.downloadCodePushUpdate(pullRequest);

      if (state.status === CodePushStatus.DOWNLOADED) {
        // Step 3: Restart app
        console.log("Download complete, restarting app...");
        await manager.restartApp();
      }
    } else if (state.status === CodePushStatus.NOT_RUNNING) {
      // Step 2: Trigger new build
      console.log("No update found, triggering new build...");
      const buildInfo = await manager.triggerBuild(pullRequest);
      console.log("Build triggered:", buildInfo.buildId);

      // Step 3: Monitor build status (you'd need to poll this)
      // Once build completes, repeat from Step 1
    } else if (state.status === CodePushStatus.ALREADY_RUNNING) {
      // Option: Cancel running build
      console.log("Build already running, canceling...");
      await manager.cancelBuild(pullRequest);
    }
  } catch (error) {
    console.error("Workflow error:", error);
  }
};

Lifecycle Callbacks

Each step in the workflow triggers specific callbacks:

const config = {
  // ... other config
  callbacks: {
    // Pull Request Management
    onPullRequestsFetched: (prs) => {
      console.log(`Fetched ${prs.length} pull requests`);
    },

    // Build Lifecycle
    onBuildTriggered: (pr, buildInfo) => {
      console.log(`Build ${buildInfo.buildId} triggered for PR #${pr.number}`);
      showNotification(`Building ${pr.title}...`);
    },

    // CodePush Discovery
    onCodePushAvailable: (pr, packageInfo) => {
      console.log(
        `CodePush available for PR #${pr.number}: ${packageInfo.label}`
      );
      updateUI(`Update available: ${packageInfo.label}`);
    },

    // Download Lifecycle
    onDownloadStarted: (pr, packageInfo) => {
      console.log(`Starting download: ${packageInfo.label}`);
      showProgressBar(0);
    },

    onDownloadProgress: (pr, progress) => {
      console.log(`Download progress: ${progress}%`);
      updateProgressBar(progress);
    },

    onDownloadComplete: (pr, localPackage) => {
      console.log(`Download complete: ${localPackage.label}`);
      hideProgressBar();

      // Store version information
      const version = manager.formatVersionForStorage(localPackage);
      AsyncStorage.setItem("app_center_version", version);

      showNotification("Update downloaded! Tap to restart.");
    },

    // State Management
    onStateChange: (pr, newState, oldState) => {
      console.log(
        `PR #${pr.number} state: ${oldState.status} → ${newState.status}`
      );
      updateButtonText(getButtonText(newState.status));
    },

    // Error Handling
    onError: (error, context, metadata) => {
      console.error(`Error in ${context}:`, error);
      showErrorNotification(`Failed to ${context}: ${error.message}`);
    },
  },
};

State-Based UI Updates

Use the state to update your UI appropriately:

const getButtonText = (status) => {
  switch (status) {
    case CodePushStatus.NOT_CHECKED:
      return "Check Update";
    case CodePushStatus.AVAILABLE:
      return "Download";
    case CodePushStatus.DOWNLOADED:
      return "Restart App";
    case CodePushStatus.ALREADY_RUNNING:
      return "Cancel Build";
    case CodePushStatus.NOT_RUNNING:
      return "Trigger Build";
    case CodePushStatus.ERROR:
      return "Retry";
    default:
      return "Status";
  }
};

const getStatusColor = (status) => {
  switch (status) {
    case CodePushStatus.AVAILABLE:
      return "#4CAF50"; // Green
    case CodePushStatus.DOWNLOADED:
      return "#2196F3"; // Blue
    case CodePushStatus.ALREADY_RUNNING:
      return "#FF9800"; // Orange
    case CodePushStatus.ERROR:
      return "#F44336"; // Red
    default:
      return "#757575"; // Gray
  }
};

This gives you complete control over the CodePush lifecycle, whether you prefer the automated approach or need fine-grained control over each step.

API Reference

CodePushManager

The main class that handles CodePush operations.

Methods

fetchPullRequests(options?: FetchPROptions): Promise<GithubPullRequest[]>

Fetches pull requests from GitHub.

const prs = await manager.fetchPullRequests({
  state: "open",
  per_page: 20,
  sort: "updated",
  direction: "desc",
});
checkCodePushUpdate(pullRequest: GithubPullRequest): Promise<CodePushOptionState>

Checks if a CodePush update is available for a specific pull request.

const state = await manager.checkCodePushUpdate(pullRequest);
console.log("Update available:", state.status === "AVAILABLE");
downloadCodePushUpdate(pullRequest: GithubPullRequest): Promise<CodePushOptionState>

Downloads the CodePush update for a pull request.

const state = await manager.downloadCodePushUpdate(pullRequest);
// Monitor progress through callbacks
triggerBuild(pullRequest: GithubPullRequest): Promise<BuildInfo>

Triggers a build for the pull request branch.

const buildInfo = await manager.triggerBuild(pullRequest);
console.log("Build triggered:", buildInfo.buildId);
cancelBuild(pullRequest: GithubPullRequest): Promise<void>

Cancels a running build.

await manager.cancelBuild(pullRequest);
processWorkflow(pullRequest: GithubPullRequest): Promise<CodePushOptionState>

Automatically determines and executes the next logical action in the CodePush workflow based on the current state. This is the recommended method for most use cases.

const newState = await manager.processWorkflow(pullRequest);
getState(pullRequestId: number): CodePushOptionState

Gets the current state for a pull request.

const state = manager.getState(pullRequest.id);

useCodePushManager Hook

React hook for managing CodePush state.

const { manager, stateMap, fetchPullRequests, resetState } =
  useCodePushManager(config);

Returns

  • manager: CodePushManager instance or null
  • stateMap: Record of pull request states keyed by PR ID
  • fetchPullRequests: Self-contained function to fetch pull requests with guaranteed callback setup
  • resetState: Function to reset state for a specific PR or all PRs

fetchPullRequests Function

The fetchPullRequests function returned by the hook is the recommended way to fetch pull requests as it ensures callbacks are properly set up before making the API call:

const { fetchPullRequests } = useCodePushManager(config);

// Automatically ensures callbacks are ready before fetching
const pullRequests = await fetchPullRequests({
  state: "open",
  per_page: 20,
  sort: "updated",
  direction: "desc",
});

Benefits over calling manager.fetchPullRequests() directly:

  • ✅ No timing issues - callbacks are guaranteed to be ready
  • ✅ No need to check if manager exists
  • ✅ Self-healing - automatically sets up callbacks if needed
  • ✅ Cleaner API - no race conditions

State Management Functions

The hook provides convenient functions for managing PR states:

const { resetState } = useCodePushManager(config);

// Reset a specific PR's state
resetState(pullRequest.id);

// Reset all PR states
resetState();

Callbacks

The library provides extensive callbacks for monitoring operations:

interface CodePushCallbacks {
  onPullRequestsFetched?: (prs: GithubPullRequest[]) => void;
  onBuildTriggered?: (pr: GithubPullRequest, buildInfo: BuildInfo) => void;
  onCodePushAvailable?: (
    pr: GithubPullRequest,
    packageInfo: RemotePackage
  ) => void;
  onDownloadStarted?: (
    pr: GithubPullRequest,
    packageInfo: RemotePackage
  ) => void;
  onDownloadProgress?: (pr: GithubPullRequest, progress: number) => void;
  onDownloadComplete?: (
    pr: GithubPullRequest,
    localPackage: LocalPackage
  ) => void;
  onError?: (error: Error, context: string, metadata?: any) => void;
  onStateChange?: (
    pr: GithubPullRequest,
    newState: CodePushOptionState,
    oldState: CodePushOptionState
  ) => void;
}

Common CI/CD Integrations

Jenkins Integration

const jenkinsProvider = createCustomCICDProvider({
  triggerBuild: async (params) => {
    const response = await fetch(
      `${JENKINS_URL}/job/${JOB_NAME}/buildWithParameters`,
      {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`${JENKINS_USER}:${JENKINS_TOKEN}`)}`,
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          BRANCH: params.branch,
          APP_VERSION: params.version,
        }),
      }
    );

    // Get build number from Location header
    const location = response.headers.get("Location");
    const buildNumber = location?.split("/").pop();

    return {
      buildId: buildNumber,
      status: { status: "queued" },
      startedAt: new Date().toISOString(),
    };
  },
  // ... other methods
});

Azure DevOps Integration

const azureDevOpsProvider = createCustomCICDProvider({
  triggerBuild: async (params) => {
    const response = await fetch(
      `https://dev.azure.com/${ORGANIZATION}/${PROJECT}/_apis/build/builds?api-version=7.0`,
      {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`:${AZURE_PAT}`)}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          definition: { id: BUILD_DEFINITION_ID },
          sourceBranch: `refs/heads/${params.branch}`,
          parameters: JSON.stringify({
            appVersion: params.version,
          }),
        }),
      }
    );

    const build = await response.json();
    return {
      buildId: build.id.toString(),
      status: { status: build.status },
      startedAt: build.queueTime,
    };
  },
  // ... other methods
});

CircleCI Integration

const circleCIProvider = createCustomCICDProvider({
  triggerBuild: async (params) => {
    const response = await fetch(
      `https://circleci.com/api/v2/project/github/${OWNER}/${REPO}/pipeline`,
      {
        method: "POST",
        headers: {
          Authorization: `Basic ${btoa(`${CIRCLE_TOKEN}:`)}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          branch: params.branch,
          parameters: {
            app_version: params.version,
          },
        }),
      }
    );

    const pipeline = await response.json();
    return {
      buildId: pipeline.id,
      status: { status: "running" },
      startedAt: new Date().toISOString(),
    };
  },
  // ... other methods
});

Error Handling

The library provides comprehensive error handling:

const config = {
  // ... other config
  callbacks: {
    onError: (error, context, metadata) => {
      console.error(`Error in ${context}:`, error);

      // Handle specific contexts
      switch (context) {
        case "fetchPullRequests":
          // Handle PR fetch errors
          break;
        case "triggerBuild":
          // Handle build trigger errors
          break;
        case "downloadCodePushUpdate":
          // Handle download errors
          break;
      }

      // Log to crash reporting service
      crashlytics().recordError(error);
    },
  },
};

TypeScript Support

The library is written in TypeScript and provides full type definitions:

import type {
  CodePushManagerConfig,
  GithubPullRequest,
  CodePushStatus,
  CodePushOptionState,
} from "@fancode/react-native-codepush-joystick";

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Support

If you encounter any issues or have questions, please file an issue on the GitHub repository.