diff --git a/docs/app-distribution/ci-tools/bitbucket.md b/docs/app-distribution/ci-tools/bitbucket.md new file mode 100644 index 0000000000..38cefec0a6 --- /dev/null +++ b/docs/app-distribution/ci-tools/bitbucket.md @@ -0,0 +1,44 @@ +--- +id: bitbucket +title: Bitbucket Pipelines +sidebar_label: Bitbucket Pipelines +description: Upload iOS and Android builds to Mobile App Distribution automatically from your Bitbucket Pipelines. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Set up Bitbucket Pipelines to upload your build artifacts (IPA or APK) directly to Sauce Labs Mobile App Distribution for distribution. + +## Setting Up + +1. Open your Bitbucket repository, and select **Settings** > **Pipelines** > **Environment Variables** + + screenshot of Bitbucket pipelines + +2. Fill in **variable** and **value**: + + - **Variable**: TESTFAIRY_API_KEY + - **Value**: _Your API key. Find it under **API Credentials** in the top navigation bar, or in the **API Key** section of **My Profile**. See [API Keys](/app-distribution/security/api-keys) for details._ + - **Secured**: Y + +3. Click **Add**. + +screenshot of Bitbucket pipelines + +4. Edit your `bitbucket-pipelines.yml` and add this command to your `script` section: + + ```bash + curl https://app.testfairy.com/api/upload -F api_key=${TESTFAIRY_API_KEY} -F file=@MyApplicationFile.apk -F format=readable + ``` + +:::caution +Do not forget to replace `MyApplicationFile.apk` with the path to your APK or IPA files. +::: + +Additional optional parameters such as `testers-groups`, `notify`, and `comment` can be added to this line. Refer to the [Upload API reference guide](/app-distribution/developer/legacy-api-v1#upload) for more information and examples. + +Here is a screenshot of a sample `bitbucket-pipelines.yml` file: + +screenshot of Bitbucket pipelines diff --git a/docs/app-distribution/ci-tools/circle-ci.md b/docs/app-distribution/ci-tools/circle-ci.md new file mode 100644 index 0000000000..4400c21a83 --- /dev/null +++ b/docs/app-distribution/ci-tools/circle-ci.md @@ -0,0 +1,38 @@ +--- +id: circle-ci +title: Circle CI +sidebar_label: Circle CI +description: Upload iOS and Android builds to Mobile App Distribution from CircleCI using the Mobile App Distribution orb. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +[CircleCI](https://circleci.com) is a cloud-based CI/CD service that helps developers automate their development process with CI hosted in the cloud or on a private server. + +Sauce Labs Mobile App Distribution has a CircleCI "ORB", allowing you to upload builds to Sauce Labs Mobile App Distribution smoothly. + +To use the ORB, add the following line to the `orbs` section of your `.circleci/config.yml`: + +```yml +orbs: + testfairy: testfairy/uploader@2.0.1 +``` + +Then, upload your .IPA or .APK, you'll have to call `testfairy/uploader`, providing the path to the file and your API key. As an example of creating, add the following command: + +```yml +jobs: + build: + # ... + steps: + # ... steps to build IPA or APK + - testfairy/uploader: + api-key: TESTFAIRY_API_KEY + file: app.apk +``` + +`TESTFAIRY_API_KEY` is the environment variable name containing your API key. Environment variables are the best practice, so you don't commit secret values into your code repository. + +You can see the complete list of supported commands by visiting the [CircleCI Sauce Labs Mobile App Distribution ORB Repository](https://circleci.com/orbs/registry/orb/testfairy/uploader). diff --git a/docs/app-distribution/ci-tools/fastlane.md b/docs/app-distribution/ci-tools/fastlane.md new file mode 100644 index 0000000000..602c5ca278 --- /dev/null +++ b/docs/app-distribution/ci-tools/fastlane.md @@ -0,0 +1,154 @@ +--- +id: fastlane +title: Fastlane +sidebar_label: Fastlane +description: Install and use the saucelabs_appdist Fastlane plugin to upload builds to Mobile App Distribution. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Upload a new build to Sauce Labs Mobile App Distribution using Fastlane and the `fastlane-plugin-saucelabs_appdist` plugin. You can find your API key under **API Credentials** in the top navigation bar, or in the **API Key** section of **My Profile**. See [API Keys](/app-distribution/security/api-keys). + +## Installation + +```bash +fastlane add_plugin saucelabs_appdist +``` + +Or add the plugin manually to your project's `fastlane/Pluginfile`: + +```ruby +gem 'fastlane-plugin-saucelabs_appdist' +``` + +Then run `bundle install` to fetch it. + +## Usage + + + + +```ruby +saucelabs_appdist( + api_key: "your_api_key", + ipa: "./path/to/app.ipa", + comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}", +) +``` + + + + +```ruby +saucelabs_appdist( + api_key: "your_api_key", + apk: "../build/app/outputs/apk/qa/release/app-qa-release.apk", + comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}", +) +``` + + + + +### Parameters + +| Key | Description | Default | +|-----------------------|----------------------------------------------------------------|---------------------------| +| `api_key` | API Key for Sauce Labs Mobile App Distribution | | +| `ipa` | Path to your IPA file (iOS) | | +| `apk` | Path to your APK file (Android) | | +| `symbols_file` | Symbols mapping file | | +| `upload_url` | Upload API URL for Sauce Labs Mobile App Distribution | `https://app.testfairy.com` | +| `testers_groups` | Array of tester groups to be notified | `[]` | +| `comment` | Additional release notes for this upload | `No comment provided` | +| `auto_update` | Auto-upgrade users (`on`/`off`) | `off` | +| `notify` | Send email to testers (`on`/`off`) | `off` | +| `options` | Array of options | `[]` | +| `custom` | Custom options string | `""` | +| `timeout` | Request timeout in seconds | | +| `tags` | Custom tags for builds | `[]` | +| `metrics` | Array of metrics to record | `[]` | +| `folder_name` | Dashboard folder name | `""` | +| `landing_page_mode` | Landing page visibility (`open`/`closed`) | `open` | +| `upload_to_saucelabs` | Upload to Sauce Labs (`on`/`off`) | `off` | +| `platform` | Platform override | `""` | +| `community_token` | Custom URL token for the landing page | `""` | +| `app_description` | Description text to display on the landing page | `""` | + +:::note +If your server's security settings require users to login before downloading, you must set `landing_page_mode: "closed"`. Otherwise the upload will fail with error code 156. +::: + +### Lane Variables + +The `saucelabs_appdist` action stores the full API response in `lane_context`, which can be accessed in subsequent actions or lanes: + +```ruby +lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE] +``` + +The response is a hash containing all fields from the upload API, including: + +| Key | Description | +|------------------------------------|---------------------------------------------------| +| `status` | Upload status (`ok` on success) | +| `build_id` | ID of the uploaded build | +| `project_id` | ID of the project | +| `app_name` | Name of the uploaded app | +| `app_version` | Version of the uploaded app | +| `file_size` | Size of the uploaded file in bytes | +| `build_url` | URL for the sessions of the newly uploaded build | +| `download_page_url` | URL of the download page | +| `app_url` | Direct download URL for the build | +| `invite_testers_url` | URL to invite testers to this build | +| `icon_url` | URL of the app icon | +| `options` | Configured options for this build | +| `platform` | Platform (iOS/Android) | +| `tags` | Tags associated with the build | +| `metadata` | Metadata associated with the build | +| `has_testfairy_sdk` | Whether the app includes the TestFairy SDK | +| `symbols_download_url` | URL to download symbols file (if uploaded) | +| `landing_page_url` | URL of the build's landing page | +| `build_specific_landing_page_url` | Landing page URL specific to this build | +| `attachments` | Attachments associated with the build | +| `landing_page_mode` | Landing page visibility (`open` or `closed`) | +| `app_description` | Description text displayed on the landing page | + +Example: + +```ruby +response = lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE] +puts response['build_url'] +puts response['app_url'] +puts response['landing_page_url'] +``` + +### Documentation + +To show the documentation in your terminal, run + +```bash +fastlane action saucelabs_appdist +``` + +### CLI + +It is recommended to add the above action into your Fastfile, however sometimes you might want to run one-offs. To do so, you can run the following command from your terminal + +```bash +fastlane run saucelabs_appdist +``` + +To pass parameters, make use of the `:` symbol, for example + +```bash +fastlane run saucelabs_appdist api_key:"your_key" ipa:"./app.ipa" +``` + +It's important to note that the CLI supports primitive types like integers, floats, booleans, and strings. Arrays can be passed as a comma delimited string (e.g. `param:"1,2,3"`). Hashes are not currently supported. + +It is recommended to add all fastlane actions you use to your Fastfile. + +You can find the plugin on [RubyGems](https://rubygems.org/gems/fastlane-plugin-saucelabs_appdist) and [GitHub](https://github.com/saucelabs/fastlane-plugin-saucelabs_appdist). \ No newline at end of file diff --git a/docs/app-distribution/ci-tools/gitlab.md b/docs/app-distribution/ci-tools/gitlab.md new file mode 100644 index 0000000000..d19bcfadb7 --- /dev/null +++ b/docs/app-distribution/ci-tools/gitlab.md @@ -0,0 +1,47 @@ +--- +id: gitlab +title: Gitlab +sidebar_label: Gitlab +description: Deploy iOS and Android builds to Mobile App Distribution automatically from a GitLab CI/CD pipeline. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +To automatically deply your Android or iOS Apps to [Sauce Labs Mobile App Distribution](https://app.testfairy.com/) by using GitLab, follow the steps below: + +1. In Sauce Labs Mobile App Distribution, click **API Credentials** in the top navigation bar, or click the **Profile** icon in the top-right corner and select **My Profile**. Your key is in the **API Key** section. + + API Key section on the My Profile page + +2. Copy your API key and go to your application's project **Settings** > **CI/CD** > **Variables** in GitLab. +3. Add a variable called `TESTFAIRY_API_KEY` to the list with the value of your API key. + + gitlab secret keys + + - To deploy, add a job to your `.gitlab-ci.yml` configuration using [fastlane](https://docs.fastlane.tools/getting-started/ios/beta-deployment/) or `curl` (example below). + + ```yaml + stages: + - deploy + + deploy: + stage: deploy + only: + - master + script: + - | + curl \ + -A "GitLab CI" \ + -F api_key="${TESTFAIRY_API_KEY}" \ + -F comment="GitLab Pipeline build ${CI_COMMIT_SHA}" \ + -F file=@android.apk \ + https://app.testfairy.com/api/upload/ + ``` + +:::note +Replace the `-F file=@android.apk` argument with a path to your APK or IPA. +::: + +For a complete list of available options, visit the [Upload API reference guide](/app-distribution/developer/legacy-api-v1#upload). diff --git a/docs/app-distribution/ci-tools/team-city.md b/docs/app-distribution/ci-tools/team-city.md new file mode 100644 index 0000000000..074dbfdc03 --- /dev/null +++ b/docs/app-distribution/ci-tools/team-city.md @@ -0,0 +1,48 @@ +--- +id: team-city +title: TeamCity +sidebar_label: TeamCity +description: Deploy iOS and Android builds to Mobile App Distribution automatically from a TeamCity build configuration. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +To automatically deply your Android or iOS Apps to [Sauce Labs Mobile App Distribution](https://www.testfairy.com/) by using TeamCity, follow the steps below: + +1. In Sauce Labs Mobile App Distribution, click the **Profile** icon in the top-right corner and select **My Profile**. + + User menu with My Profile highlighted + +2. On the **My Profile** page, go to the **API Key** section and click the **copy icon** to copy the API key. You can also copy it from **API Credentials** in the top navigation bar. + + API Key section on the My Profile page + +3. In TeamCity, add an environment variable as a **New Parameter** into the **Build Configuration**. + + build configuration + +4. Name the parameter `env.TESTFAIRY_API_KEY` and give it the value you copied from the Sauce Labs Mobile App Distribution **My Profile** page, and Save. + +  add environment variable + +5. Add a **Build Step** to the **Build Configuration** you wish to deploy from. + + add build step + +6. Make sure to select a **Command Line** build step. + + command line build step + + Copy the following command into the **Custom script** text field: + + ```bash + curl https://app.testfairy.com/api/upload -F api_key=${env.TESTFAIRY_API_KEY} -F comment="TeamCity build" -F file=@android.apk + ``` + + :::note + Replace the `-F file=@android.apk` argument with a path to your own APK or IPA. + ::: + +For a complete list of available options, visit the [Sauce Labs Mobile App Distribution Upload API documentation](/app-distribution/developer/legacy-api-v1#upload). diff --git a/docs/app-distribution/developer/api-migration-guide.md b/docs/app-distribution/developer/api-migration-guide.md new file mode 100644 index 0000000000..41deda4a67 --- /dev/null +++ b/docs/app-distribution/developer/api-migration-guide.md @@ -0,0 +1,109 @@ +--- +id: api-migration-guide +title: Migrating from the legacy API to v3 +sidebar_label: API Migration Guide +description: Learn what changed between the legacy Mobile App Distribution API and API v3, and how to migrate your scripts. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +## What changed + +- **Single versioned prefix.** Everything now lives under `/api/v3/*`. The split between `/api/1` and `/api/2` is gone. +- **JSON-everywhere.** All endpoints accept and return JSON, except `POST /api/v3/builds/upload`, which uses `multipart/form-data`. The legacy form-encoded body conventions (with `webhook-name`, `webhook-url` aliases, comma-separated `actions`, etc.) are dropped. +- **Stricter validation.** Webhook URLs are SSRF-checked. +- **Sites became Teams.** The Sites collection in v1 is the Teams collection in v3. + +## Endpoint map + +### Builds + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/projects/{pid}/builds` | `GET /api/v3/projects/{pid}/builds` | — | +| `GET /api/1/projects/{pid}/builds/{bid}` | `GET /api/v3/builds/{id}` | Path flattened | +| `PATCH /api/1/projects/{pid}/builds/{bid}` | `PUT /api/v3/builds/{id}` | Method PATCH → PUT | +| `DELETE /api/1/projects/{pid}/builds/{bid}` | `DELETE /api/v3/builds/{id}` | — | +| `POST /api/1/projects/{pid}/builds/{bid}/copy` | `POST /api/v3/builds/{id}/copy` | JSON body, no `folder_name` required | +| `GET /api/1/projects/{pid}/builds/{bid}/download` | `GET /api/v3/builds/{id}/download` | Returns a JSON `url` instead of a redirect | +| `POST /api/1/projects/{pid}/builds/{bid}/invites` | `POST /api/v3/builds/{id}/notify-testers` | Renamed to reflect what it actually does | +| `POST /api/upload` | `POST /api/v3/builds/upload` | `team_id` required unless `project_id` is given; `folder_name` → `folder`; `app_version` → `version`; `release_notes` only (no `changelog`/`comment` aliases); `groups` only (no `app_permission_groups`); returns `201` with the v3 build object | +| `GET /api/1/projects/{pid}/builds/{bid}/symbols/download` | `GET /api/v3/builds/{id}/symbols/download` | Path flattened | + +Tags change from a comma-separated string (v1) to a JSON array (v3). + +### Projects + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/projects` | `GET /api/v3/projects` | v3 shape; v1's stripped-down shape is gone | +| `GET /api/2/projects` | `GET /api/v3/projects` | — | +| `GET /api/2/projects/{pid}` | `GET /api/v3/projects/{id}` | — | +| `GET /api/2/projects/{pid}/builds` | `GET /api/v3/projects/{id}/builds` | — | +| `GET /api/2/projects/{pid}/testers` | `GET /api/v3/projects/{id}/testers` | Direct project_tester rows + group members, deduped | + +### Testers + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/testers` | `GET /api/v3/testers` | — | +| `POST /api/1/testers` | `POST /api/v3/testers` | JSON body | +| `GET /api/1/testers/{id}` | `GET /api/v3/testers/{id}` | — | +| `DELETE /api/1/testers/{id}` | `DELETE /api/v3/testers/{id}` | — | +| `POST /api/1/testers/{id}/block` | `POST /api/v3/testers/{id}/block` | — | +| `DELETE /api/1/testers/{id}/block` | `DELETE /api/v3/testers/{id}/block` | — | + +:::caution +v3 tester IDs are **membership IDs**, not user IDs. Reusing a v1 tester ID hits the wrong tester or returns `404`. Look testers up with `GET /api/v3/testers?search=` and use the returned `id`; `user_id` is also returned. +::: + +### Groups + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/groups` | `GET /api/v3/groups` | — | +| `GET /api/1/groups/{gid}` | `GET /api/v3/groups/{id}` | — | +| `GET /api/1/groups/{gid}/testers` | `GET /api/v3/groups/{id}/testers` | — | +| `GET /api/1/groups/{gid}/projects` | `GET /api/v3/groups/{id}/projects` | — | +| `GET /api/1/testers/groups` | `GET /api/v3/groups` | Moved out of the testers namespace | +| `POST /api/1/testers/groups` | `POST /api/v3/groups` | Requires `team_id` | +| `POST /api/1/testers/groups/{gid}` | `POST /api/v3/groups/{id}/testers` | JSON body with `email` | +| `DELETE /api/1/testers/groups/{gid}` | `DELETE /api/v3/groups/{id}/testers/{userId}` | The user ID (`user_id` from `/api/v3/testers`) is now in the path | + +### Webhooks + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/webhooks` | `GET /api/v3/webhooks` | — | +| `POST /api/1/webhooks` | `POST /api/v3/webhooks` | JSON body; `actions` validated against `upload`, `download`, `new-udid` | +| `GET /api/1/webhooks/{id}` | `GET /api/v3/webhooks/{id}` | — | +| `POST /api/1/webhooks/{id}` | `PUT /api/v3/webhooks/{id}` | Method POST → PUT; `status` must be `active` or `suspended` | +| `DELETE /api/1/webhooks/{id}` | `DELETE /api/v3/webhooks/{id}` | — | + +### Sites → Teams + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/sites` | `GET /api/v3/teams` | Response shape changes: `{site:{accounts,managers}}` becomes `{teams, pagination}` | +| `GET /api/1/sites/{id}` | `GET /api/v3/teams/{id}` | — | +| `POST /api/1/sites` | `POST /api/v3/teams` | — | +| `DELETE /api/1/sites/{id}` | `DELETE /api/v3/teams/{id}` | — | + +### Audit + +| Legacy | v3 | Notes | +| --- | --- | --- | +| `GET /api/1/audits` | `GET /api/v3/audits` | — | +| `GET /api/2/audits` | `GET /api/v3/audits` | — | +| `GET /api/2/audits/admin-trail` | `GET /api/v3/audits?action=` | No role-based split — filter by the specific `action_type` string. Use `GET /api/v3/audits/actions` to enumerate valid values. | +| `GET /api/2/audits/tester-trail` | `GET /api/v3/audits?action=` | Same as above — pick an `action_type` from `GET /api/v3/audits/actions`. | + +v3 action filters take one value, so make one call per action type to rebuild a trail. + +## Removed without replacement + +- `GET /api/1/cpanel/permissions` — listed org admins under a permission shape that v3 doesn't track. Use `GET /api/v3/testers` instead. + +## Watching usage + +Every hit to a legacy route is logged at `INFO` with event `legacy_api_hit`, including the route name, HTTP method, user ID, an 8-char hash of the API key, and a duration in milliseconds. If you administer an org and want to know which of your integrations are still on the deprecated surface, search logs for `legacy_api_hit` filtered by `api_key_hash`. diff --git a/docs/app-distribution/developer/api-reference.md b/docs/app-distribution/developer/api-reference.md new file mode 100644 index 0000000000..6f449b2219 --- /dev/null +++ b/docs/app-distribution/developer/api-reference.md @@ -0,0 +1,224 @@ +--- +id: api-reference +title: API Reference (v3) +sidebar_label: API Reference +description: Authenticate, paginate and call the Mobile App Distribution REST API v3 endpoints for apps, builds, teams, testers and more. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export const M = ({children}) => { + const colors = { + GET: '#3b82f6', + POST: '#22a06b', + PUT: '#f59e0b', + PATCH: '#f59e0b', + DELETE: '#ef4444', + }; + return ( + + {children} + + ); +}; + +Use the REST API to integrate app distribution into your CI/CD pipeline. + +## Authentication + +All API requests require authentication via one of the following methods: + +| Method | Example | +| --- | --- | +| `X-API-Key` header | `curl -H "X-API-Key: YOUR_KEY" ...` | +| `Bearer` token | `curl -H "Authorization: Bearer TOKEN" ...` | +| `api_key` form parameter | `curl -F api_key=YOUR_KEY ...` | +| HTTP Basic (`email:apiKey`) | `curl -u you@example.com:YOUR_KEY ...` | +| OIDC | `curl -H "Authorization: Bearer JWT" -H "X-OIDC-Config-Key: CONFIG_KEY" ...` | + +Find your API key by clicking the key icon in the top navigation bar. You can exchange it for a short-lived Bearer token via `POST /api/v3/auth/token`. + +## Pagination + +All list endpoints support pagination via query parameters: + +| Parameter | Default | Description | +| --- | --- | --- | +| `page` | 1 | Page number | +| `per_page` | 25 | Results per page (max: 100) | + +Larger `per_page` values are capped at 100, except on `/api/v3/audits`, which returns a validation error instead. + +Paginated responses include a `pagination` object. The list key matches the resource - `projects`, `builds`, `teams`, `testers`, `groups`, `webhooks`, or `audits`: + +```json +{ + "projects": [...], + "pagination": { + "page": 1, + "per_page": 25, + "total": 142, + "total_pages": 6 + } +} +``` + +## Postman Collection + +Import the collection into Postman to start testing immediately. Set the `base_url` and `api_key` variables after importing. + + + + Download Postman Collection + + +## Interactive Documentation + +For the full interactive API documentation with request/response examples, visit the **Swagger UI**. + +## Endpoints + +### Authentication + +| Method | Endpoint | Description | +| --- | --- | --- | +| POST | `/api/v3/auth/token` | Exchange API key for a 1-hour Bearer token | + +### Apps + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/projects` | List all apps (paginated) | +| GET | `/api/v3/projects/{id}` | Get an app | +| POST | `/api/v3/projects` | Create an app | +| PUT | `/api/v3/projects/{id}` | Update an app | +| DELETE | `/api/v3/projects/{id}` | Delete an app (admin) | +| GET | `/api/v3/projects/{id}/testers` | List testers assigned to an app (direct + via groups, deduped) | +| POST | `/api/v3/projects/{id}/copy` | Copy an app | + +### Builds + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/projects/{projectId}/builds` | List builds for an app (paginated) | +| GET | `/api/v3/builds/{id}` | Get a build | +| POST | `/api/v3/builds/upload` | Upload a new build (multipart/form-data) | +| PUT | `/api/v3/builds/{id}` | Update release notes and tags | +| GET | `/api/v3/builds/{id}/download` | Get pre-signed download URL. If storage isn't configured, returns the install-page URL instead. | +| DELETE | `/api/v3/builds/{id}` | Delete a build (admin) | +| POST | `/api/v3/builds/{id}/copy` | Duplicate a build within the same app (references the same file) | +| POST | `/api/v3/builds/{id}/notify-testers` | Queue the new-build email to testers. Returns `202 {"status":"queued"}`, or `409` if the build isn't distributable. Requires admin rights on the app. | +| GET | `/api/v3/builds/{id}/symbols/download` | Get a download URL for the build's symbols file | + +#### Upload Parameters + +`POST /api/v3/builds/upload` takes `multipart/form-data`: + +| Parameter | Required | Description | +| --- | --- | --- | +| `file` | Yes | The build file (`.apk`, `.aab`, `.ipa`, or `.zip`) | +| `project_id` | See note | The app to upload to | +| `team_id` | See note | Required unless `project_id` is given | +| `version` | No | Override the detected version string | +| `release_notes` | No | Release notes for the build | +| `folder` | No | Folder to place the app in | +| `groups` | No | Tester groups to notify | +| `notify` | No | Set to `1` to email testers about the new build | +| `symbols_file` | No | Symbols file to attach to the build | +| `sync_to_saucelabs` | No | Set to `1` to also copy the build to Sauce Labs App Storage | +| `landing_page_slug` | No | URL alias for the app's landing page | +| `landing_page_mode` | No | Landing page visibility | + +Provide either `project_id` or `team_id`. `PUT /api/v3/builds/{id}` accepts `release_notes`, `tags`, `landing_page_slug`, and `landing_page_mode`. + +### Teams + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/teams` | List all teams (paginated) | +| GET | `/api/v3/teams/{id}` | Get a team | +| POST | `/api/v3/teams` | Create a team (admin) | +| PUT | `/api/v3/teams/{id}` | Update a team (admin) | +| DELETE | `/api/v3/teams/{id}` | Delete a team (admin) | + +### Testers + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/testers` | List testers (paginated, searchable) | +| GET | `/api/v3/testers/{id}` | Get a tester | +| POST | `/api/v3/testers` | Invite a tester by email (admin). Only the Tester role can be created via the API. | +| DELETE | `/api/v3/testers/{id}` | Remove a tester (admin) | +| POST | `/api/v3/testers/{id}/block` | Block a tester (admin) | +| DELETE | `/api/v3/testers/{id}/block` | Unblock a tester (admin) | + +### Groups + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/groups` | List all groups (paginated) | +| GET | `/api/v3/groups/{id}` | Get a group with testers and apps | +| POST | `/api/v3/groups` | Create a group (admin) | +| PUT | `/api/v3/groups/{id}` | Update a group (admin) | +| DELETE | `/api/v3/groups/{id}` | Delete a group (admin) | +| POST | `/api/v3/groups/{id}/testers` | Add tester to group (admin) | +| DELETE | `/api/v3/groups/{id}/testers/{userId}` | Remove tester from group (admin) | +| GET | `/api/v3/groups/{id}/testers` | List testers in a group (paginated) | +| GET | `/api/v3/groups/{id}/projects` | List apps the group has access to (paginated) | + +### Webhooks + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/webhooks` | List all webhooks (paginated) | +| GET | `/api/v3/webhooks/{id}` | Get a webhook | +| POST | `/api/v3/webhooks` | Create a webhook (admin) | +| PUT | `/api/v3/webhooks/{id}` | Update a webhook (admin) | +| DELETE | `/api/v3/webhooks/{id}` | Delete a webhook (admin) | + +### Settings + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/settings/oidc` | Get the organization's OIDC configuration (admin) | +| POST | `/api/v3/settings/oidc` | Create or update the OIDC configuration (admin) | +| DELETE | `/api/v3/settings/oidc` | Delete the OIDC configuration (admin) | +| POST | `/api/v3/settings/oidc/test` | Test OIDC discovery against the issuer (admin) | + +### Audit Logs + +| Method | Endpoint | Description | +| --- | --- | --- | +| GET | `/api/v3/audits` | List audit logs (paginated, filterable by action/search/date, admin) | +| GET | `/api/v3/audits/actions` | List distinct audit action types (admin) | diff --git a/docs/app-distribution/developer/legacy-api-v1.md b/docs/app-distribution/developer/legacy-api-v1.md new file mode 100644 index 0000000000..9ebf093434 --- /dev/null +++ b/docs/app-distribution/developer/legacy-api-v1.md @@ -0,0 +1,349 @@ +--- +id: legacy-api-v1 +title: Legacy API (v1 Compatibility) +sidebar_label: Legacy API (v1) +description: Reference for the legacy v1 API, which keeps existing TestFairy CI/CD scripts and plugins working in Mobile App Distribution. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export const M = ({children}) => { + const colors = { + GET: '#3b82f6', + POST: '#22a06b', + PUT: '#f59e0b', + PATCH: '#f59e0b', + DELETE: '#ef4444', + }; + return ( + + {children} + + ); +}; + +If you are migrating from TestFairy, your existing CI/CD scripts and plugins will continue to work without changes. The legacy API endpoints are fully supported alongside the new [API v3](/app-distribution/developer/api-reference). + +## Authentication + +All endpoints require authentication. You can authenticate using any of the following methods: + +| Method | Example | +| --- | --- | +| `X-API-Key` header | `curl -H "X-API-Key: YOUR_KEY" ...` | +| `Bearer` token | `curl -H "Authorization: Bearer YOUR_KEY" ...` | +| `api_key` POST param | `curl -F api_key=YOUR_KEY ...` | + +## Response Format + +All responses return JSON with a `status` field (`"ok"` or `"fail"`). + +**Success** + +```json +{ "status": "ok", ... } +``` + +**Error** + +```json +{ "status": "fail", "code": 5, "message": "..." } +``` + +## Upload + +### POST `/api/upload` + +Upload an APK, AAB, or IPA file. The app is automatically matched by package name, or created if it doesn't exist. + +```bash +curl https://saucelabs-poc.testfairy.com/api/upload \ + -F api_key=YOUR_API_KEY \ + -F file=@app-release.apk \ + -F changelog="Bug fixes and improvements" \ + -F notify=on \ + -F testers_groups="QA,Beta" +``` + +| Parameter | Required | Description | +| --- | --- | --- | +| `file` | Yes | Binary file (`.apk`, `.aab`, or `.ipa`) | +| `changelog` | No | Release notes. Also accepted as `comment` or `release_notes` | +| `notify` | No | Set to `on` or `1` to email testers about the new build | +| `testers_groups` | No | Comma-separated group names to notify. Also accepted as `groups` or `invitation_groups` | +| `app_version` | No | Override the auto-detected version string | +| `version_code` | No | Override the auto-detected version code | +| `folder_name` | No | Assign the app to a folder | + +## Projects + +### GET `/api/1/projects/` + +List all apps in the organization. + +```bash +curl -H "X-API-Key: YOUR_KEY" https://saucelabs-poc.testfairy.com/api/1/projects/ +``` + +Response includes: `id`, `name`, `packageName`, `platform`, `icon`, `folder_name`, `created`. + +## Builds + +### GET `/api/1/projects/{projectId}/builds/` + +List all builds for an app. + +### GET `/api/1/projects/{projectId}/builds/{buildId}` + +Get a single build. + +Response includes: `id`, `projectId`, `appName`, `appVersion`, `appVersionCode`, `filesize`, `iconUrl`, `fileName`, `uploadedAt`, `uploadedVia`, `installsCount`, `tags`, `releaseNotes`, `installLink`. + +### PATCH `/api/1/projects/{projectId}/builds/{buildId}/` + +Update a build's metadata. + +| Parameter | Description | +| --- | --- | +| `comment` | Update release notes | +| `tags` | Comma-separated tags | + +### DELETE `/api/1/projects/{projectId}/builds/{buildId}` + +Delete a build. Requires admin permissions. + +### GET `/api/1/projects/{projectId}/builds/{buildId}/download/` + +Get the download URL for a build. Returns a pre-signed URL or install page link. + +### POST `/api/1/projects/{projectId}/builds/{buildId}/invites/` + +Send install invitations to testers for a build. + +## Testers + +### GET `/api/1/testers` + +List all testers in the organization. + +Response includes: `id`, `email`, `name`. + +### POST `/api/1/testers/` + +Add a tester. Creates the user if they don't exist. Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `email` | Yes | Tester's email address | +| `group` | No | Group name to add the tester to | + +### GET `/api/1/testers/{testerId}` + +Get a single tester's details. + +### DELETE `/api/1/testers/{testerId}` + +Remove a tester from the organization. Requires admin permissions. + +### POST `/api/1/testers/{testerId}/block/` + +Block a tester. Requires admin permissions. + +### DELETE `/api/1/testers/{testerId}/block/` + +Unblock a tester. Requires admin permissions. + +## Tester Groups + +### GET `/api/1/testers/groups` + +List all tester groups. Response includes: `id`, `name`, `testersCount`. + +### POST `/api/1/testers/groups` + +Create a tester group. Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `groupName` | Yes | Name for the new group | + +### POST `/api/1/testers/groups/{groupId}` + +Add a tester to a group by email. Auto-creates the tester if they don't exist. Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `email` | Yes | Tester's email address | + +### DELETE `/api/1/testers/groups/{groupId}` + +Remove a tester from a group by email. Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `email` | Yes | Tester's email address (POST body or query param) | + +## Groups + +### GET `/api/1/groups/` + +List all groups in the organization. Response includes: `id`, `name`, `testersCount`. + +### GET `/api/1/groups/{groupId}` + +Get a single group. + +### GET `/api/1/groups/{groupId}/testers/` + +List all testers in a group. Response includes: `id`, `email`, `name`. + +### GET `/api/1/groups/{groupId}/projects/` + +List all apps assigned to a group. Response includes: `id`, `name`, `packageName`, `platform`. + +## Webhooks + +### GET `/api/1/webhooks/` + +List all webhooks for the organization. + +Response includes: `id`, `name`, `url`, `status`, `actions`, `projectIds`, `createdAt`. + +### POST `/api/1/webhooks/` + +Create a webhook. Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `url` | Yes | Webhook callback URL | +| `name` | No | Display name (defaults to URL) | +| `actions` | No | Comma-separated event types to listen for | +| `project_ids` | No | Comma-separated app IDs (empty = all apps) | + +### GET `/api/1/webhooks/{webhookId}` + +Get a single webhook. + +### POST `/api/1/webhooks/{webhookId}` + +Update a webhook. Requires admin permissions. Accepts the same parameters as create (all optional). + +### DELETE `/api/1/webhooks/{webhookId}` + +Delete a webhook. Requires admin permissions. + +## Sites (Teams) + +In the legacy API, "sites" correspond to "teams" in the current platform. + +### GET `/api/1/sites/` + +List all sites (teams) in the organization. + +Response includes: `id`, `name`, `projectsCount`, `membersCount`. + +### GET `/api/1/sites/{siteId}` + +Get a single site (team). + +### POST `/api/1/sites/` + +Create a site (team). Requires admin permissions. + +| Parameter | Required | Description | +| --- | --- | --- | +| `name` | Yes | Site (team) name | + +## Audit Logs + +Requires admin permissions. All audit endpoints support the following query parameters: + +| Parameter | Description | +| --- | --- | +| `page` | Page number (default: 1) | +| `limit` | Results per page (default: 25, max: 100) | +| `action` | Filter by action type | +| `search` | Search in email and action data | +| `from` | Start date (ISO 8601) | +| `to` | End date (ISO 8601) | + +### GET `/api/1/audits/` + +List audit log entries. + +### GET `/api/2/audits/` + +List audit log entries (v2 format with pagination metadata). + +### GET `/api/2/audits/admin-trail/` + +List admin activity audit trail. + +### GET `/api/2/audits/tester-trail/` + +List tester activity audit trail. + +Response includes: `id`, `user` (id, email), `ipAddress`, `action`, `data`, `createdAt`, plus `pagination` object. + +## Error Codes + +| Code | HTTP Status | Meaning | +| --- | --- | --- | +| `1` | 400 | Missing or invalid required parameter | +| `2` | 400 | Duplicate resource (already exists) | +| `5` | 401/403 | Invalid API key or insufficient permissions | +| `112` | 400 | Empty file uploaded | +| `121` | 400 | Invalid file type | +| `133` | 400 | Organization not configured (no team found) | +| `404` | 404 | Resource not found | + +## CI/CD Examples + +**Gradle (Android)** + +```bash +curl https://saucelabs-poc.testfairy.com/api/upload \ + -F api_key=$API_KEY \ + -F file=@app/build/outputs/apk/release/app-release.apk \ + -F changelog="$(git log -1 --pretty=%B)" +``` + +**Xcode (iOS)** + +```bash +curl https://saucelabs-poc.testfairy.com/api/upload \ + -F api_key=$API_KEY \ + -F file=@build/MyApp.ipa \ + -F changelog="$(git log -1 --pretty=%B)" \ + -F notify=on +``` + +**List apps and builds** + +```bash +# List apps +curl -H "X-API-Key: $API_KEY" https://saucelabs-poc.testfairy.com/api/1/projects/ + +# List builds for an app +curl -H "X-API-Key: $API_KEY" https://saucelabs-poc.testfairy.com/api/1/projects/123/builds/ + +# Get download URL +curl -H "X-API-Key: $API_KEY" https://saucelabs-poc.testfairy.com/api/1/projects/123/builds/456/download/ +``` + +## Migration to API v3 + +To migrate, see the [API Migration Guide](/app-distribution/developer/api-migration-guide). diff --git a/docs/app-distribution/general/getting-started.md b/docs/app-distribution/general/getting-started.md new file mode 100644 index 0000000000..ecc0b83c1a --- /dev/null +++ b/docs/app-distribution/general/getting-started.md @@ -0,0 +1,125 @@ +--- +id: getting-started +title: Welcome to Mobile App Distribution +sidebar_label: Getting Started +description: Get started with Mobile App Distribution and share iOS and Android builds with your team and testers without the app stores. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Mobile App Distribution shares iOS and Android builds with your team and testers without going through the app stores. Upload a build, choose who can install it, and send them a link. + +A typical first distribution takes four steps: + +
+ + + + + + + + + Upload a build + + + Assign a tester group + + + Share the link + + + Testers install + + + + + +
+ +## Key Concepts + +| Concept | What it is | +| --- | --- | +| **Apps** | Each app holds all builds for a specific mobile application. An app belongs to exactly one team. | +| **Builds** | Upload `.ipa` (iOS), or `.apk` / `.aab` (Android) files, up to 4 GB. `.aab` files are converted to an APK for installation. Each upload with a new version creates a new build with version info, release notes, and an install link. Uploading a version that already exists replaces that build, unless **Allow duplicate versions** is turned on in the app's settings. | +| **Teams** | Isolated departments in your organization. Apps belong to teams, and members can belong to one or more teams. | +| **Tester Groups** | Named groups of testers. A group belongs to one team and can only be assigned to apps in that team. | +| **Landing Pages** | Each app gets a customizable install page with a unique URL and a QR code. | + +## Quick Start + +### 1. Upload a Build + +Open **App Management** in the sidebar, then drag and drop your `.apk`, `.aab`, or `.ipa` file onto the upload area. To add a build to an app you already have, open that app and select **Upload Build**. + +Files can be up to 4 GB. App Distribution reads the package name, platform, version, and build number from the file, so you don't need to enter them. + +See [Uploading Builds](/app-distribution/projects/uploading-builds). + +### 2. Create a Tester Group + +Select **Testers** in the sidebar, open the **Groups** tab, and select **New Group**. If you belong to more than one team, choose the team the group belongs to. + +Add existing testers, or invite new ones by email. A group can only be assigned to apps in its own team. + +See [Tester Groups](/app-distribution/organization/tester-groups). + +### 3. Assign the Group to Your App + +Open the app and select the **Testers** tab. Choose your group from the **Select a group** dropdown list, pick whether it gets every build or one specific build, then select **Assign Build(s)**. + +Only groups belonging to the same team as the app are available for assignment. + +### 4. Share the Install Link + +Open the app and select **Landing Page** ▸ **Edit** to set the URL alias and visibility, then share the link or its QR code. + +Testers open the link on their device and install the app. + +See [Landing Pages](/app-distribution/projects/landing-pages) and [Installing Apps](/app-distribution/general/installing-apps). + +:::note +Choose **Closed Beta** visibility to require testers to sign in before installing. iOS ad-hoc and development builds are always closed beta. See [Closed Beta](/app-distribution/projects/closed-beta). +::: + +## Role Hierarchy + +Each higher role inherits all permissions of the roles below it. + +| Role | Permissions | +| --- | --- | +| Account Owner | Full access to everything in the organization, including organization settings, SSO, OIDC, integrations, and the audit log. | +| Org Admin | Create and manage teams, invite and manage organization members, plus organization settings, SSO, OIDC, integrations, and the audit log. | +| Team Admin | Invite new users to their team as Members, add existing users to the team, and set their team role. Assigned per-team by an admin or by another Team Admin. | +| Member | Create apps, upload builds, create tester groups, and invite testers in their team. | +| Tester | View and install apps assigned to them directly, through a tester group, or through a build invite. Can be shared across teams. | + +For the full breakdown, see [Members and Roles](/app-distribution/organization/members-roles). + +## Automate Uploads + +After you have distributed a build by hand, move the upload into your pipeline. Builds upload through the REST API with your API key: + +```bash +curl -X POST https://your-org.testfairy.com/api/v3/builds/upload \ + -H "X-API-Key: $API_KEY" \ + -F project_id=$PROJECT_ID \ + -F file=@app-release.apk \ + -F release_notes="$(git log -1 --pretty=%B)" +``` + +Find your API key under **My Profile** ▸ **API Key**. See the [API Reference](/app-distribution/developer/api-reference) for the full endpoint list. + +## Next Steps + +| If you want to | Go to | +| --- | --- | +| Understand build states and expiry | [Build Lifecycle](/app-distribution/projects/build-lifecycle) | +| Organize people into teams | [Managing Teams](/app-distribution/organization/managing-teams) | +| Control who is notified about new builds | [Notifications](/app-distribution/organization/notifications) | +| Review who did what in your organization | [Audit Log](/app-distribution/organization/audit-log) | +| Sign in with your identity provider | [SSO / SAML](/app-distribution/settings/sso-saml) | +| Store builds in your own bucket | [Custom Storage](/app-distribution/integrations/custom-storage) | +| Post build events to Slack or Teams | [Webhooks](/app-distribution/integrations/webhooks) | +| Publish to the stores | [Google Play](/app-distribution/integrations/google-play) · [Apple App Store](/app-distribution/integrations/apple-app-store) | diff --git a/docs/app-distribution/general/installing-apps.md b/docs/app-distribution/general/installing-apps.md new file mode 100644 index 0000000000..01e9a50cf4 --- /dev/null +++ b/docs/app-distribution/general/installing-apps.md @@ -0,0 +1,38 @@ +--- +id: installing-apps +title: Installing Apps on Your Device +sidebar_label: Installing Apps +description: Install an app on your iOS or Android device from an install link, landing page or QR code. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +You can install an app on your device using the install link or landing page shared with you. You can also scan the QR code on the landing page to open the installation page on your device. + +## Install an App on iOS + +1. Open the install link on your iPhone or iPad using **Safari**. +2. Tap **Install on iOS** on the landing page. To download the `.ipa` file instead, tap **Download IPA** (shown when the app's landing page enables it). +3. A system prompt will ask you to confirm the installation - tap **Install**. +4. The app will appear on your home screen. You may need to trust the developer certificate before opening it. + +:::caution iOS Note +iOS ad-hoc and development builds always require you to log in before installing. Enterprise and App Store builds follow the app's landing-page **Visibility** setting (Open Beta or Closed Beta). Make sure you have an account and are assigned to the app, either directly, through a tester group, or through a build invite. +::: + +Installing App + +## Install an App on Android + +1. Open the install link on your Android device. +2. Tap **Install on Android**. If the build is download-only, the button reads **Download**. +3. If prompted, allow installation from unknown sources in your device settings. +4. Open the downloaded `.apk` file and follow the installation prompts. + +Installing App + +## Install an App Using a QR Code + +Each landing page includes a QR code. Scan it with your device camera to open the install page directly on your phone. + +Installing App diff --git a/docs/app-distribution/integrations/apple-app-store.md b/docs/app-distribution/integrations/apple-app-store.md new file mode 100644 index 0000000000..300354e510 --- /dev/null +++ b/docs/app-distribution/integrations/apple-app-store.md @@ -0,0 +1,104 @@ +--- +id: apple-app-store +title: Apple App Store Integration +sidebar_label: Apple App Store +description: Connect Mobile App Distribution to App Store Connect with an API key and publish iOS builds directly. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Connect Mobile App Distribution to your App Store Connect account using an API key, then publish iOS builds to App Store Connect directly from Mobile App Distribution. + +:::info +Only **Account Owners** and **Org Admins** can configure the Apple App Store integration. +::: + +## Prerequisites + +- An [App Store Connect](https://appstoreconnect.apple.com/) account with the role **Admin** or **Account Holder** (only these can create API keys) +- The app(s) you want to publish must already exist in App Store Connect with a known bundle ID + +:::important + +Apple allows you to download the `.p8` private key only once. Download and securely store the file when you create the API key. + +::: + +## Generate an App Store Connect API Key + +1. Open [App Store Connect → Users and Access → Integrations → App Store Connect API](https://appstoreconnect.apple.com/access/integrations/api). +2. Click **+** to generate a new key. Give it a name (e.g. "Mobile App Distribution Publish") and choose the **App Manager** role (or higher) so it can upload builds. +3. Click **Generate**. +4. **Download the .p8 file immediately.** Apple only allows downloading it once. +5. Note the **Key ID** shown in the table. +6. Note the **Issuer ID** shown at the top of the page (a UUID). + +## Connecting Mobile App Distribution + +After you have the API key details, add them to Mobile App Distribution. + +**Step 1:** Click the **Profile** icon in the top-right corner and select **Integrations** from the user menu. + +Apple App Store Integration + +**Step 2:** On the **Integrations** page, find **Apple App Store** under **Distribution** and select **Connect**. The **Apple App Store Integration** page opens. + +Apple App Store Integration + +**Step 3:** Under **Credentials**, enter the following information: + +| **Sr. No.** | **Field** | **Description** | +|---:|---|---| +| **1** | **Issuer ID** | Enter the UUID associated with your App Store Connect account. | +| **2** | **Key ID** | Enter the ID of the App Store Connect API key. | +| **3** | **Private Key (.p8)** | Upload the `.p8` private key file that you downloaded when you created the API key. | + +Apple App Store Integration + +**Step 4:** Click **Save**. Mobile App Distribution generates a JWT and calls App Store Connect to check the credentials before saving them. If the check fails, nothing is saved. On success, the status shows **Connected**. + +Apple App Store Integration + +## Connection Statuses + +The **Connection Status** section shows the current state of the integration. + +| **Sr. No.** | **Status** | **Meaning** | +|---:|---|---| +| **1** | **Not Configured** | App Store Connect credentials have not been configured. | +| **2** | **Connected** | The saved credentials have been successfully verified. | +| **3** | **Failed** | The connection test could not verify the credentials. Check the API key details and try again. | + +Once credentials are saved, you can: + +- **Test Connection** - re-check the saved credentials against App Store Connect. +- **Update Credentials** - replace the Issuer ID, Key ID or `.p8` file. +- **Remove Configuration** - delete the stored credentials. + +## Security + +- The `.p8` private key is **encrypted at rest** using libsodium. +- The key is never displayed or downloadable from the UI after upload. +- JWTs signed for App Store Connect API expire after 20 minutes (Apple's max). +- All credential changes are recorded in the **Audit Log**. + +## Troubleshooting + +| Error | Fix | +| --- | --- | +| `Invalid .p8 file: missing PEM header` | Make sure you uploaded the `.p8` file Apple gave you, not a converted/re-encoded version. | +| `Authentication failed: Apple rejected the credentials` | Check that the Issuer ID, Key ID, and .p8 file all belong to the same key. They are shown together on the App Store Connect API Keys page. | +| `Authorization failed: this API key does not have permission` | Promote the key's role to **App Manager** or higher in App Store Connect. | +| `Network error contacting App Store Connect` | Outbound HTTPS to `api.appstoreconnect.apple.com` must be allowed. Check firewall / egress rules. | + +## Publish a Build + +Once the credentials are saved, Account Owners and Org Admins can publish an iOS build to App Store Connect: + +**Step 1:** Open the app and find the `.ipa` build you want to publish. + +**Step 2:** Click the **⋯** (more actions) icon on the build's row and select **Publish to App Store**. + +**Step 3:** Upload the build's `AppStoreInfo.plist` file and confirm. + +Publishing runs in the background. Check the [Audit Log](/app-distribution/organization/audit-log) for the result. diff --git a/docs/app-distribution/integrations/custom-storage.md b/docs/app-distribution/integrations/custom-storage.md new file mode 100644 index 0000000000..505e668ea8 --- /dev/null +++ b/docs/app-distribution/integrations/custom-storage.md @@ -0,0 +1,166 @@ +--- +id: custom-storage +title: Custom Storage (Bring Your Own Bucket) +sidebar_label: Custom Storage +description: Store your organization's build files and app icons in your own cloud storage bucket. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Store your organization's build files and app icons in your own cloud storage bucket instead of the platform default. This gives you full control over where your data lives, for compliance, data sovereignty, or integration with your existing infrastructure. + +:::info +Only Account Owners and Org Admins can configure storage settings. Go to **Profile** menu → **Integrations** → **Custom Storage (BYOB)**. +::: + +## How It Works + +When your organization configures a custom storage bucket: + +1. **New uploads** (builds and icons) are stored in your bucket instead of the platform default. +2. **Downloads** generate time-limited presigned URLs pointing to your bucket. +3. **Existing files** uploaded before the configuration remain on the platform default storage. +4. Each build tracks which bucket it was uploaded to, so downloads always resolve to the correct location. + +## Supported Providers + +| Provider | Driver | Notes | +| --- | --- | --- | +| **Amazon S3** | `s3` | Native support. Set region and leave endpoint blank. | +| **S3-Compatible** (MinIO, Wasabi, DigitalOcean Spaces) | `s3` | Set the custom endpoint URL. Uses the S3 API protocol. | +| **Google Cloud Storage** | `gcs` | Uses the S3-compatible XML API. Set endpoint to `https://storage.googleapis.com` and use HMAC credentials. | + +## Prerequisites + +- A cloud storage bucket (e.g., an S3 bucket in your AWS account) +- An IAM user or service account with access to the bucket +- Access key and secret key for that user + +## Required IAM Permissions + +The IAM user needs the following permissions on your bucket. Note that **both the bucket ARN and the objects ARN** must be included: + +```json +{ + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ], + "Resource": "arn:aws:s3:::your-bucket-name/*" + }, + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket", + "s3:GetBucketLocation" + ], + "Resource": "arn:aws:s3:::your-bucket-name" + } + ] +} +``` + +:::caution Common mistake +Only including `arn:aws:s3:::bucket-name/*` (objects) without `arn:aws:s3:::bucket-name` (bucket). The connection test requires `s3:ListBucket` on the bucket itself (without `/*`), while uploads/downloads require permissions on the objects (with `/*`). +::: + +## Setting Up + +**Step 1:** Click the **Profile** icon in the top-right corner and select **Integrations** from the user menu. + +Custom Storage + +**Step 2:** On the **Integrations** page, find **Custom Storage (BYOB)** under **Storage** and select **Connect**. + +Custom Storage + +**Step 3:** On the **Storage Configuration** page fill in the connection details: + +| **Ref.** | **Field** | **Description** | **Example** | +|---|---|---|---| +| **1** | **Provider** | Cloud storage provider | `Amazon S3` | +| **2** | **Bucket Name** | Your storage bucket name | `my-company-builds` | +| **3** | **Region** | Bucket region. Optional; defaults to `us-east-1`. | `us-east-1`, `eu-central-1` | +| **4** | **Custom Endpoint** | Only for S3-compatible services. Leave blank for AWS S3. | `https://s3.wasabisys.com` | +| **5** | **Access Key** | IAM access key ID | `AKIAIOSFODNN7EXAMPLE` | +| **6** | **Secret Key** | IAM secret access key - encrypted at rest, never displayed after saving | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` | + +Custom Storage + +**Step 4:** Select **Save Configuration** to save your custom storage settings. + +Custom Storage + +**Step 5:** Click **Test Connection** to verify Mobile App Distribution can access your bucket. + +Custom Storage + +## Connection Status + +| **Status** | **Meaning** | +|---|---| +| **Not Configured** | Custom storage has not been configured. Files are stored using the platform's default storage. | +| **Untested** | The custom storage configuration has been saved but has not yet been verified. | +| **Connected** | The custom storage configuration has been verified and the bucket is accessible. | +| **Failed** | The custom storage connection could not be verified. Check the storage credentials, bucket name, region, and required permissions. | +| **Disabled** | Custom storage is disabled. New uploads use the platform's default storage, while existing files stored in your custom bucket remain accessible. | + +## Storage Resolution + +The platform determines where to store and retrieve files using the following logic: + +**For uploads (new builds)** + +1. If the organization has an **enabled** custom storage config → upload to the org's bucket +2. Otherwise → upload to the platform default bucket + +**For downloads (existing builds)** + +1. If the build has a linked storage config (even if disabled) → use that config's credentials to generate the download URL +2. Otherwise → use the platform default bucket + +This means **disabling your custom storage does not break existing downloads**. Each build remembers which bucket it was uploaded to, and the system uses the saved credentials to serve the file — even after the config is disabled. + +## Disabling vs. Removing + +| Action | Effect on New Uploads | Effect on Existing Files | +| --- | --- | --- | +| **Disable** | Go to platform default | Still accessible from your bucket (credentials preserved) | +| **Re-enable** | Resume uploading to your bucket | No change | +| **Remove** | Go to platform default | Only possible when no builds still use the bucket | + +:::note +You can remove the configuration only when no builds use the bucket. Otherwise disable it instead: existing builds keep downloading from the bucket, and new uploads go to the default storage. +::: + +## File Path Structure + +Files are stored using the same relative path structure in both platform default and custom buckets: + +```text +releases/{orgId}/{projectId}/{filename}-{uniqueId}.{ext} +icons/{orgId}/{projectId}/{randomHex}.png +``` + +Only relative paths are stored in the database - never full URLs. This means you can switch buckets or providers without modifying any existing data. + +## Security + +- Secret keys are **encrypted at rest** using libsodium (XSalsa20-Poly1305). +- Credentials are never stored in plain text, never logged, and never displayed in the UI after saving. +- Build download URLs are **time-limited presigned URLs** that expire after 60 minutes - they cannot be shared permanently. +- Saving, disabling, enabling and removing the storage configuration are recorded in the [Audit Log](/app-distribution/organization/audit-log). + +## Troubleshooting + +| Error | Cause | Fix | +| --- | --- | --- | +| `403 Forbidden` | IAM user lacks permissions, or the bucket ARN is missing from the policy | Check the access key, secret and bucket policy. `s3:ListBucket` on the bucket itself is required. Add both `arn:aws:s3:::bucket` and `arn:aws:s3:::bucket/*` to the IAM policy. | +| `404 Not Found` | Bucket name or region is incorrect, or the bucket doesn't exist | Check the bucket name and region | +| `Connection timed out` | Wrong region, wrong endpoint, or network restriction | Verify the region matches the bucket's actual region. Check VPC/firewall rules. | + +The connection test uses `HeadBucket`, which returns no error body, so failures surface as generic `403` or `404` responses. diff --git a/docs/app-distribution/integrations/google-play.md b/docs/app-distribution/integrations/google-play.md new file mode 100644 index 0000000000..30911a7d7b --- /dev/null +++ b/docs/app-distribution/integrations/google-play.md @@ -0,0 +1,107 @@ +--- +id: google-play +title: Google Play Integration +sidebar_label: Google Play +description: Publish APK and AAB builds from Mobile App Distribution directly to your Google Play Console. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Publish APK and AAB builds directly from Mobile App Distribution to your Google Play Console, no need to upload them manually through the web console. Configure once at the organization level and use the **Publish to Google Play** action on any Android build. + +:::info +Only **Account Owners** and **Org Admins** can configure the Google Play integration. +::: + +## Prerequisites + +- A Google Play Console account with the app already created (matching your project's package name) +- A **service account** in Google Cloud with access to the Play Console +- A JSON key file for that service account + +## Setting Up the Service Account + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create or select a project. +2. Enable the **Google Play Android Developer API**. +3. Go to **IAM & Admin → Service Accounts** and create a new service account. +4. For the new service account, go to the **Keys** tab and click **Add Key → Create new key**. Choose JSON and download the file. +5. Open the [Google Play Console](https://play.google.com/console) → **Users and permissions**. +6. Click **Invite new users** and add the service account's email (looks like `name@project.iam.gserviceaccount.com`). +7. Grant the **Admin** or **Release Manager** role with permission to upload APKs and manage releases. + +## Connecting Mobile App Distribution + +**Step 1:** Click the **Profile** icon in the top-right corner and select **Integrations** from the user menu. + +Google Play Integration + +**Step 2:** On the **Integrations** page, find **Google Play** under **Distribution** and click **Connect**. + +Google Play Integration + +**Step 3:** Under **Credentials**, upload your Google Play service account JSON file using **Choose file**. + +Google Play Integration + +The service account must have the **Admin** or **Release Manager** role in Google Play Console with permission to upload APKs/AABs and manage releases. + +:::note + +You can download the service account JSON key from **Google Cloud Console → IAM & Admin → Service Accounts → Keys**. + +::: + +**Step 4:** Click **Save**. The JSON key is checked before saving. After the credentials are saved, the integration can be used to publish Android builds to Google Play. + +The **Connection Status** shows **Connected** once the key has been verified, or **Failed** if it could not be. You can re-check a saved key with **Test Connection**, or swap it with **Replace Credentials**. + +Google Play Integration + +## Publishing a Build + +After configuring the integration, Account Owners and Org Admins can publish an Android build directly from Mobile App Distribution. Only `.apk` and `.aab` builds can be published. + +1. Open an Android app and find the build you want to publish (must be APK or AAB). +2. Click the **⋯** (more actions) icon on the build row and choose **Publish to Google Play**. +3. Pick a track and a status, then click **Publish**. +4. The publish runs in the background, Mobile App Distribution downloads the file from storage, uploads it to Google Play, and assigns it to the chosen track. +5. Check the [Audit Log](/app-distribution/organization/audit-log) for the result. Failed publishes are retried up to 3 times and logged as `google_play_publish_failed`. + +The build's release notes are sent as `en-US` release notes (first 500 characters). If the build was uploaded as an `.aab`, the original bundle is published. + +## Tracks + +The publishing flow allows you to select a Google Play track for the build. + +| **Sr. No.** | **Track** | **Audience** | +|---:|---|---| +| **1** | **Internal** | Up to 100 internal testers. | +| **2** | **Alpha** | Closed testing for invited groups. | +| **3** | **Beta** | Open or closed beta testing for a broader audience. | +| **4** | **Production** | Makes the app available to Google Play users. | + +## Statuses +You can also select a release status when publishing the build. + +| **Sr. No.** | **Status** | **Meaning** | +|---:|---|---| +| **1** | **Draft** | The release has been created but not published. It remains available in Google Play Console for review. | +| **2** | **In Progress** | The release is currently being rolled out. | +| **3** | **Halted** | The rollout has been paused. | +| **4** | **Completed** | The release has been fully rolled out. | + +## Security + +- Service account credentials are **encrypted at rest** using libsodium. +- Credentials are never displayed in the UI after upload. +- All publish actions are logged in the **Audit Log**. + +## Troubleshooting + +| Error | Fix | +| --- | --- | +| `Package not found` | The app must already exist in your Google Play Console. Service accounts cannot create new apps - only manage existing ones. | +| `The caller does not have permission` | Re-check that the service account is invited in **Play Console → Users and permissions** with sufficient role. | +| `Version code already exists` | Increment your build's `versionCode` in `build.gradle` before uploading. | +| `Invalid credentials` | The file isn't a valid service-account JSON key. Download a fresh key from Google Cloud Console. | +| `Authentication failed` | The key was revoked or disabled. Generate a new key in Google Cloud Console. | diff --git a/docs/app-distribution/integrations/saucelabs-connection.md b/docs/app-distribution/integrations/saucelabs-connection.md new file mode 100644 index 0000000000..cbd675884c --- /dev/null +++ b/docs/app-distribution/integrations/saucelabs-connection.md @@ -0,0 +1,94 @@ +--- +id: saucelabs-connection +title: Sauce Labs Connection +sidebar_label: Sauce Labs Connection +description: Connect Mobile App Distribution to your Sauce Labs account for team sync, role mapping, user provisioning and App Storage. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Connect your Mobile App Distribution organization to your Sauce Labs account to enable automatic team sync, role mapping, user provisioning, and App Storage integration. + +:::info +Only **Account Owners** and **Org Admins** can manage the Sauce Labs connection. Go to **Profile** menu → **Integrations** → **SauceLabs**. +::: + +## Setting Up the Connection + +**Step 1:** Click the **Profile** icon in the top-right corner and select **Integrations** from the user menu. + +Sauce Labs Connection + +**Step 2:** On the **Integrations** page, find **SauceLabs** and click **Connect**. + +Sauce Labs Connection + +**Step 3:** On the **SauceLabs Connection** page, click **Connect with SauceLabs**. + +Sauce Labs Connection + +**Step 4:** Sign in to your **Sauce Labs** account when prompted. + +Sauce Labs Connection + +After authentication, your Mobile App Distribution organization is connected to your Sauce Labs organization. + +## What Happens When Connected + +Once the connection is established, the following features are enabled: + +| Feature | Description | +| --- | --- | +| **Team Sync** | Teams from your SauceLabs organization are automatically created in Mobile App Distribution. Users are added to their SauceLabs teams on every login. | +| **Role Mapping** | SauceLabs roles are mapped to Mobile App Distribution roles on login. Org Admins in SauceLabs become Org Admins in Mobile App Distribution. Team members become Members. | +| **Auto-Provisioning** | New SauceLabs users get a Mobile App Distribution account automatically when they log in via SauceLabs SSO for the first time. | +| **App Storage** | Builds can be copied to SauceLabs App Storage using the uploading user's credentials. Copying is opt-in per upload. | + +## Copying Builds to App Storage + +Copying a build to SauceLabs App Storage is opt-in, not automatic. To copy a build, check **Also upload to SauceLabs App Storage** when uploading it, or send `sync_to_saucelabs=1` through the API. + +You must have signed in with Sauce Labs at least once, so that your Sauce Labs credentials are stored. If they aren't, the copy is skipped silently. + +## Role Mapping + +Sauce Labs roles are mapped to Mobile App Distribution roles as follows: + +| SauceLabs Role | Mobile App Distribution Role | +| --- | --- | +| Organization Admin | Org Admin, plus Team Admin of their synced teams | +| Team Admin | Member, plus Team Admin of their synced teams | +| Everyone else | Member | + +:::note +While **Sync roles** is on, an existing Tester role is overwritten with Member. The **Account Owner** role in Mobile App Distribution is not changed by role synchronization. +::: + +## Team Sync Behavior + +When team synchronization is active: + +- On every SauceLabs login, the user's team memberships are synced. +- New SauceLabs teams are automatically created in Mobile App Distribution. +- On each sign-in, the user is removed from every team whose name isn't in their Sauce Labs team list, including teams created in App Distribution. +- If a user has no remaining SauceLabs teams, their Mobile App Distribution account is blocked (they cannot log in). +- If they are re-added to a team in SauceLabs, their account is unblocked on next login. +- Blocking and unblocking happen only when **Sync teams** is on. + +## Sync Settings + +After connecting, you can turn these options on or off on the SauceLabs settings page. They take effect only after you click **Save Settings**. + +- **Sync teams** - Enable/disable automatic team creation and membership sync. +- **Sync roles** - Enable/disable role mapping on login. +- **Auto-provision** - Enable/disable automatic account creation for new SauceLabs users. + +## Navigation Changes + +While connected, the **Users** menu item opens Sauce Labs team management, and **Invite User** is hidden. Teams are still managed in App Distribution. + +Connecting also adds **Mobile Devices** to the sidebar, and **Run on SauceLabs** to the build actions. + +## Disconnecting + +You can disconnect from SauceLabs at any time from the settings page. Disconnecting stops team/role sync and auto-provisioning, but does not remove existing users or teams from Mobile App Distribution. diff --git a/docs/app-distribution/integrations/smtp-email.md b/docs/app-distribution/integrations/smtp-email.md new file mode 100644 index 0000000000..a836f3883b --- /dev/null +++ b/docs/app-distribution/integrations/smtp-email.md @@ -0,0 +1,197 @@ +--- +id: smtp-email +title: SMTP Integration +sidebar_label: SMTP Email +description: Send build notifications, invitations and other Mobile App Distribution emails through your own SMTP server. +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +Configure your own SMTP server to send all outgoing emails (build notifications, invitations, app assignments) through your mail provider. Each organization can have its own SMTP configuration, with a global fallback for organizations that don't. + +:::info +Only **Account Owners** and **Org Admins** can configure SMTP settings. Go to **Profile** menu → **Integrations** → **SMTP**. +::: + +## Prerequisites + +- An SMTP server (e.g., Amazon SES, SendGrid, Mailgun, Gmail, or your corporate mail server) +- SMTP credentials: host, port, username, and password +- A verified sender email address (required by most providers to avoid spam filtering) + +## Setting Up SMTP + +**Step 1:** Click the **Profile** icon in the top-right corner and select **Integrations** from the user menu. + +SMTP / Email + +**Step 2:** On the **Integrations** page, find **SMTP / Email** under **Communication** and click **Connect**. + +SMTP / Email + +**Step 3:** In the **SMTP Configuration** page, enter your SMTP server details. + +|Ref.| Field | Description | Example | +|---| --- | --- | --- | +|1. | **SMTP Host** | Your mail server hostname | `email-smtp.us-east-1.amazonaws.com` | +| 2. | **SMTP Port** | Server port (default: 587) | `587` | +| 3. | **Username** | SMTP authentication username | `your-smtp-username` | +| 4. | **Password** | SMTP authentication password | Stored encrypted — never shown after saving | +| 5. | **Encryption** | Connection security: TLS, SSL, or None | `TLS` (recommended) | +| 6. | **From Address** | Sender email on outgoing messages | `noreply@yourcompany.com` | +| 7. | **From Name** | Sender display name (optional) | `Your Company` | + +SMTP / Email + +**Step 4:** Click **Save Configuration** to save your SMTP settings. + +SMTP / Email + +**Step 5:** Click **Test Connection** to send a test email to your own address and verify the setup. + +SMTP / Email + +## Connection Status + +After saving, the SMTP settings page shows one of three statuses: + +| Status | Meaning | +| --- | --- | +| **Untested** | Configuration saved but not yet verified | +| **Connected** | Test email sent successfully - SMTP is working | +| **Failed** | Test failed - check credentials and server settings. The error message is shown below the status. | + +## How Emails Are Sent + +All outgoing emails are processed asynchronously through a background queue. When an email is triggered (e.g., a build upload notification), it is placed in the queue and delivered shortly after. If delivery fails, the system retries up to **3 times** with exponential backoff before marking it as failed. + +This means email sending never blocks the UI or API — uploads and other actions complete immediately while notifications are delivered in the background. The test email is the exception: it is sent immediately, while all other emails are queued. + +## Per-Organization SMTP + +Each organization can configure its own SMTP server. When sending an email, the system follows this resolution order: + +1. **Organization SMTP** - if the organization has saved an SMTP configuration, use it. +2. **Global SMTP** - fall back to the platform-wide SMTP configuration. +3. **Default mailer** - if no SMTP is configured at any level, use the platform default. + +This allows different organizations to send emails from their own domains (e.g., `noreply@company-a.com` vs `noreply@company-b.com`) while sharing the same platform. + +:::caution +Once organization SMTP is saved, all organization email is sent through it, even if the test failed. There is no fallback, so test your settings before saving. +::: + +## Security + +SMTP passwords are **encrypted at rest** using libsodium (XSalsa20-Poly1305). They are never stored in plain text, never logged, and never displayed in the UI after saving. Only the mail-sending service decrypts them at the moment of delivery. + +## Common SMTP Providers + +| Provider | Host | Port | Encryption | +| --- | --- | --- | --- | +| Amazon SES | `email-smtp.{region}.amazonaws.com` | 587 | TLS | +| SendGrid | `smtp.sendgrid.net` | 587 | TLS | +| Mailgun | `smtp.mailgun.org` | 587 | TLS | +| Gmail / Google Workspace | `smtp.gmail.com` | 587 | TLS | +| Microsoft 365 | `smtp.office365.com` | 587 | TLS | + +## Disconnecting + +Click **Remove Configuration** on the SMTP settings page to remove the configuration. Emails will fall back to the global SMTP or the platform default. + +## Custom Email Templates + +Create fully custom HTML email templates for every type of email your organization sends. Replace the default system templates with your own branded, custom-designed emails using simple `{variable}` placeholders. + +:::info +Email templates are available after you configure organization SMTP. Account Owners and Org Admins manage them in the **Email Templates** section at the bottom of the **SMTP** page. If you remove the SMTP configuration, custom templates stop being used. +::: + +### How It Works + +Once organization SMTP is configured, you can override the default system emails with your own complete HTML body. Each template type has its own set of **variables** that get replaced with real values when the email is sent. + +Variables use the `{variable_name}` syntax - just place them anywhere in your HTML subject line or body and they'll be substituted automatically. + +### Template Types + +| Type | When It's Sent | +| --- | --- | +| **Build Notification** | When a new build is uploaded and testers are notified | +| **Member Invitation** | When a new member (non-tester) is invited to the organization | +| **Tester Invitation** | When a new tester is invited to the organization or added to a group | +| **App Assignment** | When testers in a group are notified about a newly assigned app | + +SMTP / Email + +### Available Variables + +Each template type has its own set of variables: + +**Build Notification** + +| Variable | Description | +| --- | --- | +| `{app_name}` | App's display name | +| `{version}` | Build version number | +| `{build_number}` | Build code / number | +| `{platform}` | Platform (iOS or Android) | +| `{install_url}` | Download / install URL | +| `{tester_name}` | Tester's first name | +| `{tester_email}` | Tester's email address | +| `{release_notes}` | Build release notes | +| `{organization_name}` | Organization name | +| `{package_name}` | App's package name or bundle ID | +| `{file_size}` | Build file size | +| `{tags}` | Build tags | +| `{organization_subdomain}` | Organization subdomain | +| `{login_url}` | Login URL | +| `{unsubscribe_url}` | Unsubscribe URL | + +**Member Invitation** + +| Variable | Description | +| --- | --- | +| `{organization_name}` | Organization name | +| `{role_name}` | Assigned role name | +| `{accept_url}` | Invitation acceptance URL | + +**Tester Invitation** + +| Variable | Description | +| --- | --- | +| `{organization_name}` | Organization name | +| `{accept_url}` | Invitation acceptance URL | +| `{developer_email}` | Email address of the person sending the invitation | +| `{tester_email}` | Tester's email address | +| `{organization_subdomain}` | Organization subdomain | +| `{login_url}` | Login URL | + +**App Assignment** + +| Variable | Description | +| --- | --- | +| `{app_name}` | App's display name | +| `{organization_name}` | Organization name | +| `{tester_name}` | Tester's first name | +| `{install_url}` | Install URL | + +### Managing Templates + +1. Go to **Profile** menu → **Integrations** → **SMTP**, and scroll to **Email Templates**. +2. Click **Customize** on the template type you want to change. Once a custom template exists, the button reads **Edit**. +3. Edit the **Subject Line** and **HTML Body**. Click variables in the right panel to insert them at your cursor position. +4. Use the **Preview** button to see a live preview with sample data. +5. Click **Save Template** when you're happy with the result. + +### Tips + +- Use **Load Default** to start from the system default template and customize from there. +- You can **Pause** a template to temporarily revert to the system default without deleting your work, and **Activate** to use it again. +- Use **Delete Template** to permanently remove the custom template and revert to the system default. +- Templates are complete HTML documents — include your own `