Skip to content
Last updated

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 section.
  • Enable Ionic Portals and Live Updates in your mobile app.
  • In addition to Live Updates, integrate ionic-cloud CLI 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
    • appflow live-update download --token <token> --app-id <appId> --channel-name <channel> --zip-name <appId>-<channel>.zip
  • Add the Capacitor plugins listed here.

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.

Step 1. Set Up MWA Infrastructure in the Partner App

  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.

iOS

Official documentation:

1. Install the SDK

Add the Ionic Portals dependency via Swift Package Manager or CocoaPods. See the 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:

// UIKit
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    PortalsRegistrationManager.shared.register(key: "YOUR_PORTALS_KEY")
    return true
}
// 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 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:

extension Portal {
    static let workouts = Portal(
        name: "workouts",
        startDir: "portals/workouts",
        liveUpdateConfig: .workoutsMwa
    )
}

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):

// SwiftUI
struct WorkoutsView: View {
    var body: some View {
        PortalView(portal: .workouts)
    }
}
// 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.

Android

Official documentation:

1. Install the SDK

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

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:

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

Add it to your AndroidManifest.xml:

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

Avoid committing your Portals key to source code repositories. Use the Secrets Gradle Plugin to keep it out of version control.

3. Create a Portal with Live Updates

PortalManager.newPortal("workouts")
    .setStartDir("851e0894-${partnerName}production")
    .setLiveUpdateConfig(
        applicationContext,
        LiveUpdate("851e0894", "${partnerName}production")
    )
    .create()

4. Display the Portal

Option A -- XML layout:

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

Option B -- Jetpack Compose:

@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:

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:

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 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.

// --- 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.

React Native

Official documentation:

1. Install the SDK

npm install @ionic/portals-react-native

2. Register your Portals key

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

await register('YOUR_PORTALS_KEY');

3. Create and render a Portal

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:

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:

// AppDelegate.h
@interface RNAppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>
// main.m
return UIApplicationMain(argc, argv, nil, NSStringFromClass([RNAppDelegate class]));

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

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 and Android to manage web asset bundling as part of the native build process for each platform, or use the Appflow CLI.

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 NameValue
Workouts MWA App Id851e0894 (startingRoute - /workouts/home)
Bioage MWA App Id068a3720 (startingRoute - /bioage/home)
NFC MWA App Iddcbe378a (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 KeyAsk EGYM: Keeper -> BMA -> <Ionic Portals Key>

Capacitor Plugins

NameDescription
@capacitor/preferencesProvides 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/appStandard plugin required for better user experience.
@capacitor/hapticsStandard plugin required for better user experience.
@capacitor/keyboardStandard plugin required for better user experience.
@capacitor/status-barStandard plugin required for better user experience.
@capacitor/browserOpens external links natively in an in-app browser (e.g., terms-of-use and privacy-policy pages hosted on external resources).
@capacitor/deviceExposes internal device information such as model, operating system version, and unique device identifiers.
@capacitor-community/sqlitePlugin for working with on-device SQLite databases. Requires the SQLCipher package.

@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.

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.

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

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

2.1.2 User has an existing EGYM ID

  • User is prompted to enter their password.

2.png

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

3.png

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.

my alt text
User journey for a new EGYM user
my alt text
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. See the 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. 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.

my alt text
User journey for an existing EGYM user
my alt text
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.

my alt text
User journey for a returning EGYM user
my alt text
Integration between mobile app and MWA

Properties That Mobile App Needs to Pass to MWA Initial Context

Property NameTypeRequired in membershipId-based linkingRequired in email-based linkingDescription
emailStringYesYesEmail of the partner app user
firstNameStringNoYesFirst name of the partner app user
lastNameStringNoYesLast name of the partner app user
dateOfBirthStringNoYesDate of birth of the user; used by the BioAge feature
gymLocationStringYesYesIdentifier 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.
languageStringYesYesUser locale and language code (e.g., "en_US"). Required for localizing the web app interface and workout-related data.
measurementSystemStringYesYesMeasurement unit system. Accepted values: "IMPERIAL" or "METRIC". Required for the workouts feature.
genderStringNoYesGender of the user ("MALE" or "FEMALE")
membershipIdStringYesNoMember ID in the partner system. Must be unique for exactly one member in the gym chain.
cardNumberStringNoYesA 12-digit number (may have leading zeroes) that corresponds to the NFC pass content. Required for NFC Login.
startingRouteStringYesYesRoute to open after the web application launches. Provided by EGYM.
clientIdStringYesYesHardcoded 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.