A flexible CodePush Joystick for React Native apps
npm install @fancode/react-native-codepush-joystickA 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.
- 🚀 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
``bash`
npm install @fancode/react-native-codepush-joystickor
yarn add @fancode/react-native-codepush-joystick
Make sure you have the required peer dependencies installed:
`bash`
npm install react react-nativeor
yarn add react react-native
`typescript
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),CodePush available for PR #${pr.number}
onCodePushAvailable: (pr, packageInfo) => {
console.log();Starting download for PR #${pullRequest.number}: ${packageInfo.label}
},
onDownloadStarted: (pullRequest, packageInfo) => {
console.log(
);
},
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 (
#{pr.number} - {pr.title}
Branch: {pr.head?.ref} | Author: {pr.user.login}
{state.message && (
)}
{state.progress !== null && (
Download Progress: {state.progress}%
)}
backgroundColor: state.loading ? "#ccc" : "#007AFF",
padding: 12,
borderRadius: 8,
marginTop: 8,
}}
disabled={state.loading}
onPress={() => manager?.processWorkflow(pr)}
>
{state.loading ? "Loading..." : buttonText}
);
};
return (
CodePush Joystick
keyExtractor={(item) => item.id.toString()}
renderItem={renderPullRequest}
/>
);
}
`
`typescript
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,
};
`
`typescript
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 (
renderItem={({ item }) => {
const state = stateMap[item.id];
return (
{item.title} - {state?.status}
{state?.status === "ERROR" && (
)}
);
}}
/>
);
}
`
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:
`typescript
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.):
`typescript
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,
},
};
},
});
`
The main configuration object for the CodePush manager:
`typescript`
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:
`typescript
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),Starting download for PR #${pullRequest.number}: ${packageInfo.label}
onDownloadStarted: (pullRequest, packageInfo) => {
console.log(
);
},
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:
`typescript
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
`typescript`
interface GitHubConfig {
owner: string; // GitHub repository owner
repo: string; // Repository name
token: string; // GitHub personal access token
}
#### GitHubActionsConfig
`typescript`
interface GitHubActionsConfig {
owner: string;
repo: string;
token: string;
workflowFile: string; // e.g., 'codepush-build.yml'
workflowInputs?: Record
}
You can implement custom versioning strategies:
`typescript${major}.${minor}.${newPatch}-${pr.head?.ref}
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 ;`
},
},
};
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:
`typescript
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
},
};
You can also manually format the version string:
`typescript
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);
},
},
};
`
The library provides both a convenience method for automatic workflow management and individual methods for granular control.
The processWorkflow method automatically determines the next action based on the current state:
`typescript`
// 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 updatesAVAILABLE
- → Download the updateDOWNLOADED
- → Restart the appALREADY_RUNNING
- → Cancel the running buildNOT_RUNNING
- → Trigger a new build
For advanced use cases, you can control each step manually:
#### 1. Check for Updates
`typescript
// 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:
`typescript
// 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
`typescript
// Trigger a new build for the pull request
const buildInfo = await manager.triggerBuild(pullRequest);
// Callbacks triggered:
// - onBuildTriggered
// - onStateChange
`
#### 4. Download Update
`typescript
// Download the CodePush update
const state = await manager.downloadCodePushUpdate(pullRequest);
// Callbacks triggered in sequence:
// - onDownloadStarted
// - onDownloadProgress (multiple times)
// - onDownloadComplete
// - onStateChange
`
#### 5. Install & Restart
`typescript`
// Restart the app to apply the update
await manager.restartApp();
`typescript
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);
}
};
`
Each step in the workflow triggers specific callbacks:
`typescriptFetched ${prs.length} pull requests
const config = {
// ... other config
callbacks: {
// Pull Request Management
onPullRequestsFetched: (prs) => {
console.log();
},
// Build Lifecycle
onBuildTriggered: (pr, buildInfo) => {
console.log(Build ${buildInfo.buildId} triggered for PR #${pr.number});Building ${pr.title}...
showNotification();
},
// CodePush Discovery
onCodePushAvailable: (pr, packageInfo) => {
console.log(
CodePush available for PR #${pr.number}: ${packageInfo.label}Update available: ${packageInfo.label}
);
updateUI();
},
// 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);Failed to ${context}: ${error.message}
showErrorNotification();`
},
},
};
Use the state to update your UI appropriately:
`typescript
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.
The main class that handles CodePush operations.
#### Methods
##### fetchPullRequests(options?: FetchPROptions): Promise
Fetches pull requests from GitHub.
`typescript`
const prs = await manager.fetchPullRequests({
state: "open",
per_page: 20,
sort: "updated",
direction: "desc",
});
##### checkCodePushUpdate(pullRequest: GithubPullRequest): Promise
Checks if a CodePush update is available for a specific pull request.
`typescript`
const state = await manager.checkCodePushUpdate(pullRequest);
console.log("Update available:", state.status === "AVAILABLE");
##### downloadCodePushUpdate(pullRequest: GithubPullRequest): Promise
Downloads the CodePush update for a pull request.
`typescript`
const state = await manager.downloadCodePushUpdate(pullRequest);
// Monitor progress through callbacks
##### triggerBuild(pullRequest: GithubPullRequest): Promise
Triggers a build for the pull request branch.
`typescript`
const buildInfo = await manager.triggerBuild(pullRequest);
console.log("Build triggered:", buildInfo.buildId);
##### cancelBuild(pullRequest: GithubPullRequest): Promise
Cancels a running build.
`typescript`
await manager.cancelBuild(pullRequest);
##### processWorkflow(pullRequest: GithubPullRequest): Promise
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.
`typescript`
const newState = await manager.processWorkflow(pullRequest);
##### getState(pullRequestId: number): CodePushOptionState
Gets the current state for a pull request.
`typescript`
const state = manager.getState(pullRequest.id);
React hook for managing CodePush state.
`typescript`
const { manager, stateMap, fetchPullRequests, resetState } =
useCodePushManager(config);
#### Returns
- manager: CodePushManager instance or nullstateMap
- : Record of pull request states keyed by PR IDfetchPullRequests
- : Self-contained function to fetch pull requests with guaranteed callback setupresetState
- : 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:
`typescript
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:
`typescript
const { resetState } = useCodePushManager(config);
// Reset a specific PR's state
resetState(pullRequest.id);
// Reset all PR states
resetState();
`
The library provides extensive callbacks for monitoring operations:
`typescript`
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;
}
`typescript${JENKINS_URL}/job/${JOB_NAME}/buildWithParameters
const jenkinsProvider = createCustomCICDProvider({
triggerBuild: async (params) => {
const response = await fetch(
,Basic ${btoa(
{
method: "POST",
headers: {
Authorization: ${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
});
`
`typescripthttps://dev.azure.com/${ORGANIZATION}/${PROJECT}/_apis/build/builds?api-version=7.0
const azureDevOpsProvider = createCustomCICDProvider({
triggerBuild: async (params) => {
const response = await fetch(
,Basic ${btoa(
{
method: "POST",
headers: {
Authorization: :${AZURE_PAT})},refs/heads/${params.branch}
"Content-Type": "application/json",
},
body: JSON.stringify({
definition: { id: BUILD_DEFINITION_ID },
sourceBranch: ,
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
});
`
`typescripthttps://circleci.com/api/v2/project/github/${OWNER}/${REPO}/pipeline
const circleCIProvider = createCustomCICDProvider({
triggerBuild: async (params) => {
const response = await fetch(
,Basic ${btoa(
{
method: "POST",
headers: {
Authorization: ${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
});
`
The library provides comprehensive error handling:
`typescriptError in ${context}:
const config = {
// ... other config
callbacks: {
onError: (error, context, metadata) => {
console.error(, 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);
},
},
};
`
The library is written in TypeScript and provides full type definitions:
`typescript`
import type {
CodePushManagerConfig,
GithubPullRequest,
CodePushStatus,
CodePushOptionState,
} from "@fancode/react-native-codepush-joystick";
1. Fork the repository
2. Create your feature branch (git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature'
3. Commit your changes ()git push origin feature/amazing-feature`)
4. Push to the branch (
5. Open a Pull Request
If you encounter any issues or have questions, please file an issue on the GitHub repository.