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-joystickPeer Dependencies
Make sure you have the required peer dependencies installed:
npm install react react-native
# or
yarn add react react-nativeQuick 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 callbackstriggerBuild(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 nullstateMap: 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
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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