JSPM

@fancode/react-native-codepush-joystick

0.0.1-beta.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 65
  • Score
    100M100P100Q83114F
  • License MIT

A flexible CodePush Joystick for React Native apps

Package Exports

  • @fancode/react-native-codepush-joystick
  • @fancode/react-native-codepush-joystick/cjs/index.js
  • @fancode/react-native-codepush-joystick/esm/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@fancode/react-native-codepush-joystick) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

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}`);
      },
    },
  };

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

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

  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?.handleButtonPress(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,
};

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),
  },
};

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",
};

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}`;
    },
  },
};

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);
handleButtonPress(pullRequest: GithubPullRequest): Promise<CodePushOptionState>

Handles the main action button press based on current state.

const newState = await manager.handleButtonPress(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 } = useCodePushManager(config);

Returns

  • manager: CodePushManager instance or null
  • stateMap: Record of pull request states keyed by PR ID

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;
  onDownloadProgress?: (pr: GithubPullRequest, progress: number) => 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.

Changelog

0.1.0

  • Initial release
  • GitHub Actions CI/CD provider
  • Custom CI/CD provider support
  • React hook for state management
  • Comprehensive callback system
  • TypeScript support