# EGYM Training Experience in the Partner Mobile Application

The EGYM training experience is delivered to partner apps as a Micro Web Application (MWA) that partners can integrate into their mobile apps. This approach provides partner app users with a cohesive experience while giving partners a framework for quick and seamless integration.

## Context

The EGYM Micro Web Application should integrate seamlessly with the partner mobile application so that users perceive it as a native part of the partner app.

The purpose of this document is to provide high-level technical guidance on the steps a partner needs to take to integrate the EGYM MWA.

## Integration between Mobile App and MWA

### Suggested Steps

**Step 1. Set up MWA infrastructure in the partner app** (can be done autonomously by the partner):

- Review the documentation in the [Ionic Portals](#ionic-portals) section.
- Enable [Ionic Portals](https://ionic.io/docs/portals/for-ios/getting-started) and [Live Updates](https://ionic.io/docs/portals/for-ios/live-updates) in your mobile app.
- In addition to [Live Updates](https://ionic.io/docs/portals/for-ios/live-updates), integrate [ionic-cloud CLI](https://ionic.io/docs/appflow/cli/reference/ionic-cloud) into your native app build CI pipeline. This ensures the MWA is available on the device without relying on Live Updates and helps reduce live update counts. Use the following command (see the table below for parameter values):
  - [Install Appflow CLI](https://ionic.io/docs/appflow/cli/overview)
  - 
```sh
appflow live-update download --token <token> --app-id <appId> --channel-name <channel> --zip-name <appId>-<channel>.zip
```
- Add the Capacitor plugins listed [here](#capacitor-plugins).


**Step 2. Integrate partner and EGYM products** (requires collaboration with EGYM to understand current and future product use cases):

This step varies from partner to partner. Treat this section as a starting point for collaborative discussion rather than a prescriptive implementation guide.

- Set up communication between the mobile app and MWA:
  - Pass [required parameters](#properties-that-mobile-app-needs-to-pass-to-mwa-initial-context) to MWA through `initialContext`.
  - Implement the Ionic Portals `dismiss` event to allow MWA to return from full screen to the native partner app, as described [here](/mwa/docs/native_interface_and_plugins#native---web-interfacecommands).
- [Provide a list of locations to map to EGYM locations](#gym-location-mappings).


## Step 1. Set Up MWA Infrastructure in the Partner App

### Ionic Portals and Related Documentation

1. Micro Web App Partner Documentation: https://developer.egym.com/mwa/docs/introduction
2. Ionic Portals Documentation: https://ionic.io/docs/portals/


### Platform-Specific Setup

The sections below provide a quick-start overview for each platform. For complete details, refer to the official Ionic Portals documentation linked in each section.

details
summary
strong
iOS
**Official documentation:**

- [Using Portals in iOS](https://ionic.io/docs/portals/for-ios/getting-started)
- [Adding Live Updates (iOS)](https://ionic.io/docs/portals/for-ios/live-updates)


#### 1. Install the SDK

Add the Ionic Portals dependency via Swift Package Manager or CocoaPods. See the [iOS Quick Start](https://ionic.io/docs/portals/for-ios/quick-start) for detailed installation instructions.

#### 2. Register your Portals key

Register as early as possible in the app lifecycle -- typically in `AppDelegate` or the SwiftUI `App` initializer:

```swift
// UIKit
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    PortalsRegistrationManager.shared.register(key: "YOUR_PORTALS_KEY")
    return true
}
```

```swift
// SwiftUI
import IonicPortals

@main
struct PartnerApp: App {
    init() {
        PortalsRegistrationManager.shared.register(key: "YOUR_PORTALS_KEY")
    }
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
```

> Avoid committing your Portals key to source code repositories. Use an [`.xcconfig` file](https://nshipster.com/xcconfig/) to keep it out of version control.


#### 3. Create a Portal with Live Updates

Define a `Portal` with a `LiveUpdate` configuration pointing to the correct Appflow app ID and channel:

```swift
extension Portal {
    static let workouts = Portal(
        name: "workouts",
        startDir: "portals/workouts",
        liveUpdateConfig: .workoutsMwa,
        plugins: [
            .type(PreferencesPlugin.self)
            // and other plugins if needed
        ]
    )
}

extension LiveUpdate {
    static let workoutsMwa = Self(
        appId: "851e0894",
        channel: "${partnerName}production",
        syncOnAdd: true
    )
}
```

When `syncOnAdd` is `true`, a sync check occurs the first time the Portal is created. Updated assets are downloaded and applied on the next Portal load.

#### 4. Display the Portal

Use `PortalView` (SwiftUI) or `PortalUIView` (UIKit):

```swift
// SwiftUI
struct WorkoutsView: View {
    var body: some View {
        PortalView(portal: .workouts)
    }
}
```

```swift
// UIKit
class WorkoutsViewController: UIViewController {
    override func loadView() {
        self.view = PortalUIView(portal: .workouts)
    }
}
```

#### 5. Add web assets

Copy the MWA web bundle into your Xcode project so it is included in `Bundle.main`. The directory name must match the Portal's `startDir`. You can automate this with a build phase script or the [Appflow CLI](#step-1-set-up-mwa-infrastructure-in-the-partner-app).

details
summary
strong
Android
**Official documentation:**

- [Using Portals in Android](https://ionic.io/docs/portals/for-android/getting-started)
- [Live Updates (Android)](https://ionic.io/docs/portals/for-android/live-updates)


#### 1. Install the SDK

Add the dependencies to your module-level `build.gradle`:

```groovy
dependencies {
    implementation 'io.ionic:portals:0.12.0'
    implementation 'io.ionic:liveupdates:0.5.7'
}
```

Ensure `mavenCentral()` is included in your project-level repositories.

#### 2. Register your Portals key

Create a custom `Application` class and register before any Portals are loaded:

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        PortalManager.register("YOUR_PORTALS_KEY")
    }
}
```

Add it to your `AndroidManifest.xml`:

```xml
<application android:name=".MyApplication" ... >
```

> Avoid committing your Portals key to source code repositories. Use the [Secrets Gradle Plugin](https://github.com/google/secrets-gradle-plugin) to keep it out of version control.


#### 3. Create a Portal with Live Updates

```kotlin
PortalManager.newPortal("workouts")
    .setStartDir("851e0894-${partnerName}production")
    .setLiveUpdateConfig(
        applicationContext,
        LiveUpdate("851e0894", "${partnerName}production")
    )
    .setPlugins(mutableListOf(
        PreferencesPlugin::class.java,
        // and other plugins if needed
    ))
    .create()
```

#### 4. Display the Portal

**Option A -- XML layout:**

```xml
<io.ionic.portals.PortalView
    app:portalId="workouts"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
```

**Option B -- Jetpack Compose:**

```kotlin
@Composable
fun WorkoutsPortal() {
    AndroidView(factory = { PortalView(it, "workouts") })
}
```

> Activities containing a Portal should add the following to `AndroidManifest.xml` to prevent WebView restarts on configuration changes:

```xml
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
```


#### 5. Configure the Gradle build file

Web assets can contain directories starting with special characters (e.g., underscores) that Android's default asset packaging omits. Add the following to `defaultConfig` in your module `build.gradle`:

```groovy
aaptOptions {
    // Override default to accommodate modern web app directory structures
    ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
```

#### 6. Automate MWA download in the Gradle build

Instead of relying solely on Live Updates at runtime, you can bundle the MWA into your APK at build time. This ensures the app works on first launch without waiting for a Live Update download and reduces your live update count.

Add the following tasks to your module-level `build.gradle`. This uses the [Appflow CLI](https://ionic.io/docs/appflow/cli/overview) to download the latest MWA build and unpack it into `src/main/assets/` before each build.

See the full reference implementation at [egym/android-mwa-reference](https://github.com/egym/android-mwa-reference/blob/main/app/build.gradle).

```groovy
// --- MWA download configuration ---
def appId = "851e0894"                  // MWA App Id (see configuration table)
def channel = "${partnerName}production" // Live Update channel
def ionicToken = ""                     // Appflow Personal Access Token

def zipName = "${appId}-${channel}.zip"
def zipFile = file("${project.projectDir}/${zipName}")
def destinationDir = file("src/main/assets/${appId}-${channel}")

tasks.register("cleanMwaAssets", Delete) {
    group = "appflow"
    description = "Clean previous Live Update assets and zip"
    delete destinationDir, zipFile
}

tasks.register("downloadMwaBuild", Exec) {
    group = "appflow"
    description = "Download Live Update zip from Appflow"
    dependsOn "cleanMwaAssets"
    workingDir project.projectDir

    doFirst {
        println "Downloading Live Update: appId=${appId}, channel=${channel}"
    }

    commandLine "bash", "-lc",
            "appflow live-update download " +
                    "--app-id=${appId} " +
                    "--channel-name=${channel} " +
                    "--token=${ionicToken} " +
                    "--zip-name=${zipName}"

    outputs.file(zipFile)
}

tasks.register("unzipMwaBuild", Copy) {
    group = "appflow"
    description = "Unzip Live Update into src/main/assets"
    dependsOn "downloadMwaBuild"

    from { zipTree(zipFile) }
    into destinationDir

    doFirst {
        destinationDir.mkdirs()
    }

    inputs.file(zipFile)
    outputs.dir(destinationDir)
}

tasks.register("cleanupMwaZip", Delete) {
    group = "appflow"
    description = "Remove downloaded zip after extraction"
    dependsOn "unzipMwaBuild"
    delete zipFile
}

// Hook into the build lifecycle so MWA is downloaded before each build
tasks.matching { it.name == "preBuild" }.all {
    dependsOn "cleanupMwaZip"
}
```

This task chain runs automatically before each build: it cleans any previously downloaded assets, downloads the latest MWA zip from Appflow, extracts it into the assets directory, and removes the zip file.

details
summary
strong
React Native
**Official documentation:**

- [Using Portals in React Native](https://ionic.io/docs/portals/for-react-native/getting-started)
- [Live Updates (React Native)](https://ionic.io/docs/portals/for-react-native/live-updates)


#### 1. Install the SDK

```bash
npm install @ionic/portals-react-native
```

#### 2. Register your Portals key

```typescript
import { register } from '@ionic/portals-react-native';

await register('YOUR_PORTALS_KEY');
```

#### 3. Create and render a Portal

```tsx
import { PortalView } from '@ionic/portals-react-native';

const workoutsPortal = {
    name: 'workouts',
    startDir: 'portals/workouts',
    initialContext: {
        // Pass initial context properties here
    },
};

<PortalView
    portal={workoutsPortal}
    style={{ flex: 1 }}
/>;
```

#### 4. Register Capacitor plugins

Capacitor plugins must be explicitly registered with both their Android class path and iOS class name:

```typescript
const workoutsPortal = {
    name: 'workouts',
    startDir: 'portals/workouts',
    plugins: [
        {
            androidClassPath: 'com.capacitorjs.plugins.preferences.PreferencesPlugin',
            iosClassName: 'CAPPreferencesPlugin',
        },
        // Add other required plugins here
    ],
};
```

#### 5. iOS-specific configuration

**AppDelegate rename:** Both Capacitor and React Native define an `AppDelegate` class. Rename yours to avoid conflicts:

```objc
// AppDelegate.h
@interface RNAppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>
```

```objc
// main.m
return UIApplicationMain(argc, argv, nil, NSStringFromClass([RNAppDelegate class]));
```

**Podfile:** Add a `pre_install` hook to compile Capacitor as a dynamic framework:

```ruby
dynamic_frameworks = ['Capacitor', 'CapacitorCordova']

pre_install do |installer|
    installer.pod_targets.each do |pod|
        if dynamic_frameworks.include?(pod.name)
            def pod.static_framework?; false; end
            def pod.build_type; Pod::BuildType.dynamic_framework; end
        end
    end
end
```

#### 6. Bundling web assets

There is currently no built-in tooling for bundling web apps as part of `@ionic/portals-react-native`. Follow the native guides for [iOS](https://ionic.io/docs/portals/for-ios/how-to/pull-in-web-bundle) and [Android](https://ionic.io/docs/portals/for-android/how-to/pull-in-web-bundle) to manage web asset bundling as part of the native build process for each platform, or use the [Appflow CLI](#step-1-set-up-mwa-infrastructure-in-the-partner-app).

### Versioning

The partner native mobile app and the Workouts MWA must use the same major versions of Ionic Portals, Capacitor Runtime, and Capacitor Core Plugins. Use the following versions:

- Ionic Portals: latest (v0.12.*)
- Capacitor Core: latest (v7.*)
- Capacitor Plugins: latest (v7.*)


### Configuration Properties for Ionic Portals Initialization and MWA Updates

| Property Name | Value |
|  --- | --- |
| Workouts MWA App Id | 851e0894 (startingRoute - /workouts/home) |
| Bioage MWA App Id | 068a3720 (startingRoute - /bioage/home) |
| NFC MWA App Id | dcbe378a (startingRoute - /nfc/home) |
| Live Update Channels | `${partnerName}develop`, `${partnerName}preprod`, `${partnerName}production` |
| Personal Access Token (needed for Ionic CLI) | Ask EGYM: `Keeper -> BMA -> <Ionic Personal Access Token>` |
| Ionic Portals Key | Ask EGYM: `Keeper -> BMA -> <Ionic Portals Key>` |


### Capacitor Plugins

| Name | Description |
|  --- | --- |
| [@capacitor/preferences](https://capacitorjs.com/docs/apis/preferences) | Provides access to mobile phone storage for preserving account linking state and EGYM API access tokens. This core plugin (https://capacitorjs.com/docs/apis) must be enabled in both the mobile application and the web application. |
| [@capacitor/app](https://capacitorjs.com/docs/apis/app) | Standard plugin required for better user experience. |
| [@capacitor/haptics](https://capacitorjs.com/docs/apis/haptics) | Standard plugin required for better user experience. |
| [@capacitor/keyboard](https://capacitorjs.com/docs/apis/keyboard) | Standard plugin required for better user experience. |
| [@capacitor/status-bar](https://capacitorjs.com/docs/apis/status-bar) | Standard plugin required for better user experience. |
| [@capacitor/browser](https://capacitorjs.com/docs/apis/browser) | Opens external links natively in an in-app browser (e.g., terms-of-use and privacy-policy pages hosted on external resources). |
| [@capacitor/device](https://capacitorjs.com/docs/apis/device) | Exposes internal device information such as model, operating system version, and unique device identifiers. |
| [@capacitor-community/sqlite](https://github.com/capacitor-community/sqlite) | Plugin for working with on-device SQLite databases. Requires the SQLCipher package. |
| [@egym/capacitor-nfc-pass-wallet](https://github.com/egym/lib-capacitor-nfc-pass-wallet) (optional) | Install only if you are using NFC MWA integration with EGYM. See [NFC Integration](#nfc-integration) for more details. |


#### @capacitor-community/sqlite additional packages:

**iOS:**

```
SQLCipher
```

**Android:**

```
net.zetetic:android-database-sqlcipher
androidx.sqlite:sqlite
androidx.security:security-crypto
```

## Step 2. Integrate Partner and EGYM Products

There are two options for integrating the partner native app with the EGYM MWA:

- **2.1 (Preferred):** Link the partner app with EGYM ID using `membershipId`.
- **2.2:** Link the partner app with EGYM ID using `email`.


### 2.1 Link Partner App with EGYM ID Using membershipId

Prerequisites:

- Integration with the partner's membership management system is required to link app profiles with EGYM ID (the user's account in the EGYM system). See https://developer.egym.com/mms-api-v2/apis/mms-v2.
- An EGYM ID (`accountId`) must be created using MMS API v2 before the member opens MWA.
- The `membershipId` must be unique within the partner's system.
- The member must complete EGYM ID registration by opening the EGYM MWA and accepting the EGYM ID terms of use.


An EGYM ID is required to access the EGYM MWA and EGYM machines. A critical part of the integration is linking user accounts from the partner mobile application (typically from the membership management system) with EGYM and persisting this link between application launches, so the user completes the linking flow only once.

The partner app shares user data with MWA to link an EGYM ID, including `email`, `firstName`, `lastName`, `dateOfBirth`, `gymLocation`, `language`, `measurementSystem`, `gender`, and a unique `membershipId` from the partner's system. See [Properties that mobile app needs to pass to MWA Initial Context](#properties-that-mobile-app-needs-to-pass-to-mwa-initial-context).

The `membershipId` maintains the link between the partner system and EGYM.

#### Securing the membershipId with JWT

To ensure a secure connection between EGYM and the partner system, the `membershipId` must be signed on the partner side and securely included in the `initialContext`:

1. **Generate a key pair.** The partner generates a public/private key pair and shares the public key with EGYM via a secure channel. EGYM stores the public key in a secret manager and uses it to decode and verify the `membershipId`.
Example -- generate JWT keys:
  - **Private Key Generation:**
    - `openssl genrsa -out ./private.key 4096`
    - This generates a `private.key` file containing your private key.
**Do not share this file. Keep it secret. If someone obtains this private key, they can impersonate your system.**
  - **Public Key Generation:**
    - `openssl rsa -in private.key -pubout -outform PEM -out public.key`
    - This creates a `public.key` file from your private key. Share this file with EGYM; it will be used to verify JWTs signed by your private key.
2. **Create a signing endpoint.** The partner implements (or extends an existing) backend endpoint that generates a short-lived JWT signed with the private key containing the `membershipId`.
Expected JWT payload:
  - `sub`: the member ID in the partner's member management system
  - `exp`: token expiration timestamp
3. **Pass the JWT at launch.** When the user opens the EGYM entry point within the partner app:
  - The partner app requests a signed `membershipId` JWT from the partner backend.
  - The JWT is included in the `initialContext` under the `membershipId` field.
  - To optimize performance, the partner can cache tokens and reuse them until they expire, reducing backend calls.
4. **EGYM verifies the JWT.** The EGYM MWA reads the `membershipId` JWT from the initial context and forwards it to the EGYM backend. The EGYM backend accepts the JWT via its passwordless authentication API and verifies the signature, ensuring the request originates from an authorized and trusted source.


![image.png](/assets/image-15.be0050a923bd39438a0daa2364a5b9fdcfae45694c849cc5fac9a06a8e6653e7.469b1fe1.png)

When the user opens the MWA, the following cases are possible:

### 2.1.1 User opens EGYM MWA for the first time

- User is notified about the creation of an EGYM ID.
- User accepts the terms of use and privacy policy.
- User accepts the health data processing consent.
- User can optionally accept sharing their data with the gym.


![1.png](/assets/1.73e1d2f8ed5abe3437031aa3ec4dac3b6de1f5f4b16bd73b44807d945b6676ac.469b1fe1.png)

### 2.1.2 User has an existing EGYM ID

- User is prompted to enter their password.


![2.png](/assets/2.df597f78bae0230ed07aed1491a88a15635a23b4ac5608064db486fec306b751.469b1fe1.png)

### 2.1.3 User has previously logged in and the partner app already knows the EGYM ID

![3.png](/assets/3.32e9fe310c8482da2a8edc325d1864ddb3dfd2f91a7eae50d4d673b965460b76.469b1fe1.png)

### 2.2 Link Partner App with EGYM ID Using Email

> **Note:** The `email` must be unique within the partner's system and must not change.


An EGYM ID is required to access the EGYM MWA and EGYM machines. A critical part of the integration is linking user accounts from the partner mobile application with EGYM and persisting this link between application launches, so the user completes the linking flow only once.

The partner app shares user data with MWA to create an EGYM user, including `email`, `firstName`, `lastName`, `dateOfBirth`, `gymLocation`, `language`, `measurementSystem`, and `gender`.

### 2.2.1 User opens EGYM MWA for the first time

The most common scenario is when a partner app user does not yet have an EGYM ID. In this case, the MWA creates an EGYM ID and exerciser profile automatically and associates it with the partner account.

figure
img
figcaption
User journey for a new EGYM user
figure
img
figcaption
Integration between mobile app and MWA
The partner application is responsible for passing a predefined set of attributes to the Workouts MWA through the [Ionic InitialContext](/mwa/docs/architecture#portals-initial-context). See the [Properties that mobile app needs to pass to MWA Initial Context](#properties-that-mobile-app-needs-to-pass-to-mwa-initial-context) section for the full list.

### 2.2.2 User has an existing EGYM ID

This scenario applies to users who already have an EGYM ID. The data structure passed through the Ionic InitialContext is the same as for [Use Case 2.2.1](#221-user-opens-egym-mwa-for-the-first-time). The MWA checks whether the user exists; if the email is found in the EGYM platform, the user is asked to enter their credentials. After authentication, the MWA triggers account linking and associates the EGYM ID with the partner account.

figure
img
figcaption
User journey for an existing EGYM user
figure
img
figcaption
Forgot password flow for an existing EGYM user
### 2.2.3 User has previously logged in and the partner app already knows the EGYM ID

This scenario applies when the user has already linked accounts and opens the app to track workouts. The MWA identifies this flow by the presence of both a refresh token and an access token in user preferences. To simplify the partner-side integration, the data structure passed through the Ionic InitialContext can be the same as for [Use Case 2.2.1](#221-user-opens-egym-mwa-for-the-first-time).

figure
img
figcaption
User journey for a returning EGYM user
figure
img
figcaption
Integration between mobile app and MWA
### Properties That Mobile App Needs to Pass to MWA Initial Context

| Property Name | Type | Required in membershipId-based linking | Required in email-based linking | Description |
|  --- | --- | --- | --- | --- |
| email | String | Yes | Yes | Email of the partner app user |
| firstName | String | No | Yes | First name of the partner app user |
| lastName | String | No | Yes | Last name of the partner app user |
| dateOfBirth | String | No | Yes | Date of birth of the user; used by the BioAge feature |
| gymLocation | String | Yes | Yes | Identifier of the partner user's gym location (from the partner system; mapped to an EGYM location during setup). Required for MWA to resolve training plan and exercise customizations. |
| language | String | Yes | Yes | User locale and language code (e.g., "en_US"). Required for localizing the web app interface and workout-related data. |
| measurementSystem | String | Yes | Yes | Measurement unit system. Accepted values: "IMPERIAL" or "METRIC". Required for the workouts feature. |
| gender | String | No | Yes | Gender of the user ("MALE" or "FEMALE") |
| membershipId | String | Yes | No | Member ID in the partner system. Must be unique for exactly one member in the gym chain. |
| cardNumber | String | No | Yes | A 12-digit number (may have leading zeroes) that corresponds to the NFC pass content. Required for NFC Login. |
| startingRoute | String | Yes | Yes | Route to open after the web application launches. Provided by EGYM. |
| clientId | String | Yes | Yes | Hardcoded application identifier. Provided by EGYM. |


## Gym Location Mappings

An important part of the integration between the partner and EGYM is configuring mappings between gym location identifiers in both systems. The EGYM backend uses gym location identifiers in user account linking flows (registration and login) as well as the workouts feature, so setting up location mappings is a prerequisite for the web app to function properly.

## NFC Integration

NFC MWA integration requires additional integration steps compared to Workouts or Bioage.
To enable NFC MWA integration, you need to:

1. Install the [@egym/capacitor-nfc-pass-wallet](https://github.com/egym/lib-capacitor-nfc-pass-wallet) plugin (see installation instructions in the plugin's README)
2. Add the plugin to the Ionic Portals Configuration for the NFC MWA App (see [Platform-Specific Setup](#platform-specific-setup))


Android:

```kotlin
.setPlugins(mutableListOf(
    PreferencesPlugin::class.java,
    CapacitorNFCPassWalletPlugin::class.java,
))
```

iOS:

```swift
let plugins: [Plugin] = [
    .type(PreferencesPlugin.self),
    .type(CapacitorNFCPassWalletPlugin.self)
]
```

The NFC MWA app can then be used to request, add and manage NFC passes in Apple/Google wallets.