From 624c34e6da4bb4f9bfa7b6e35983b1d9507bb7e7 Mon Sep 17 00:00:00 2001 From: Thor963 Date: Mon, 16 Mar 2026 02:05:33 +0530 Subject: [PATCH 1/2] feat: custom base url support --- .../java/com/stallion/StallionModule.java | 18 +++++--- .../networkmanager/StallionApiConstants.java | 16 +++++++- .../networkmanager/StallionSyncHandler.java | 2 +- .../com/stallion/storage/StallionConfig.java | 15 +++++++ .../storage/StallionConfigConstants.java | 1 + .../stallion/utils/StallionApiBaseUrl.java | 41 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 7 ---- .../xcschemes/StallionExample.xcscheme | 2 +- example/ios/StallionExample/Info.plist | 8 ---- example/src/App.tsx | 4 +- ios/main/Stallion.swift | 10 +++++ ios/main/StallionApiBaseUrl.swift | 35 ++++++++++++++++ ios/main/StallionConfig.h | 2 + ios/main/StallionConfig.m | 11 ++++- ios/main/StallionConfigConstants.h | 1 + ios/main/StallionConfigConstants.m | 1 + ios/main/StallionConstants.swift | 7 +++- ios/main/StallionObjConstants.h | 1 + ios/main/StallionObjConstants.m | 4 ++ src/main/constants/apiConstants.ts | 6 ++- .../state/actionCreators/useConfigActions.ts | 3 ++ src/main/utils/getApiBaseUrl.ts | 41 +++++++++++++++++++ src/main/utils/useApiClient.ts | 16 ++++++-- src/types/config.types.ts | 1 + src/types/utils.types.ts | 4 +- 25 files changed, 225 insertions(+), 32 deletions(-) create mode 100644 android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java create mode 100644 ios/main/StallionApiBaseUrl.swift create mode 100644 src/main/utils/getApiBaseUrl.ts diff --git a/android/src/main/java/com/stallion/StallionModule.java b/android/src/main/java/com/stallion/StallionModule.java index 14dfa84..05027b9 100644 --- a/android/src/main/java/com/stallion/StallionModule.java +++ b/android/src/main/java/com/stallion/StallionModule.java @@ -61,11 +61,19 @@ public String getName() { @ReactMethod public void onLaunch(String launchData) { - // try { - // JSONObject launchDataJson = new JSONObject(launchData); - // } catch (Exception e) { - // e.printStackTrace(); - // } + try { + if (launchData != null && !launchData.isEmpty()) { + JSONObject launchDataJson = new JSONObject(launchData); + if (launchDataJson.has("baseUrl")) { + String baseUrl = launchDataJson.getString("baseUrl"); + if (baseUrl != null && !baseUrl.isEmpty()) { + com.stallion.utils.StallionApiBaseUrl.set(baseUrl); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } stallionStateManager.setIsMounted(true); DeviceEventManagerModule.RCTDeviceEventEmitter eventEmitter = getReactApplicationContext().getJSModule( DeviceEventManagerModule.RCTDeviceEventEmitter.class diff --git a/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java b/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java index 2ba4b4f..51e92c4 100644 --- a/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java +++ b/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java @@ -22,7 +22,21 @@ public class StallionApiConstants { public static final String STALLION_SDK_TOKEN_KEY = "x-sdk-pin-access-token"; public static final String STALLION_DEVICE_ID_KEY = "uid"; + // Default constant for reference + public static final String DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io"; - public static final String STALLION_API_BASE = "https://api.stalliontech.io"; + /** + * Gets the API base URL from config or returns default + * @return String - The base URL to use + */ + public static String getStallionApiBase() { + return com.stallion.utils.StallionApiBaseUrl.get(); + } + + // Keep old constant for backward compatibility, but mark as deprecated + /** @deprecated Use getStallionApiBase() instead */ + @Deprecated + public static final String STALLION_API_BASE = DEFAULT_STALLION_API_BASE; + public static final String STALLION_INFO_API_PATH = "/api/v1/promoted/get-update-meta"; } diff --git a/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java b/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java index 512fbeb..b57fc83 100644 --- a/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java +++ b/android/src/main/java/com/stallion/networkmanager/StallionSyncHandler.java @@ -50,7 +50,7 @@ public static void sync() { // Make API call using StallionApiManager JSONObject releaseMeta = StallionApiManager.post( - StallionApiConstants.STALLION_API_BASE + StallionApiConstants.STALLION_INFO_API_PATH, + StallionApiConstants.getStallionApiBase() + StallionApiConstants.STALLION_INFO_API_PATH, requestPayload.toString() ); diff --git a/android/src/main/java/com/stallion/storage/StallionConfig.java b/android/src/main/java/com/stallion/storage/StallionConfig.java index cc5d704..d71e4ac 100644 --- a/android/src/main/java/com/stallion/storage/StallionConfig.java +++ b/android/src/main/java/com/stallion/storage/StallionConfig.java @@ -11,6 +11,9 @@ import java.util.UUID; +import com.stallion.storage.StallionConfigConstants; +import com.stallion.networkmanager.StallionApiConstants; + public class StallionConfig { private String uid; private final String projectId; @@ -22,6 +25,7 @@ public class StallionConfig { private String lastDownloadingUrl; private String lastUnverifiedHash; private final String publicSigningKey; + private String baseUrl; public StallionConfig(Context context, SharedPreferences sharedPreferences) { @@ -77,6 +81,7 @@ public StallionConfig(Context context, SharedPreferences sharedPreferences) { this.filesDirectory = context.getFilesDir().getAbsolutePath(); this.lastDownloadingUrl = sharedPreferences.getString(StallionConfigConstants.LAST_DOWNLOADING_URL_IDENTIFIER, ""); this.lastUnverifiedHash = sharedPreferences.getString(StallionConfigConstants.LAST_UNVERIFIED_HASH, ""); + this.baseUrl = sharedPreferences.getString(StallionConfigConstants.BASE_URL_IDENTIFIER, StallionApiConstants.DEFAULT_STALLION_API_BASE); } public String getLastDownloadingUrl() { @@ -143,6 +148,15 @@ public String getPublicSigningKey() { return this.publicSigningKey; } + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl != null ? baseUrl : StallionApiConstants.DEFAULT_STALLION_API_BASE; + sharedPreferences.edit().putString(StallionConfigConstants.BASE_URL_IDENTIFIER, this.baseUrl).apply(); + } + public JSONObject toJSON() { JSONObject configJson = new JSONObject(); try { @@ -151,6 +165,7 @@ public JSONObject toJSON() { configJson.put("appToken", this.appToken); configJson.put("sdkToken", this.sdkToken); configJson.put("appVersion", this.appVersion); + configJson.put("baseUrl", this.baseUrl != null ? this.baseUrl : StallionApiConstants.DEFAULT_STALLION_API_BASE); return configJson; } catch (JSONException ignored) { return new JSONObject(); diff --git a/android/src/main/java/com/stallion/storage/StallionConfigConstants.java b/android/src/main/java/com/stallion/storage/StallionConfigConstants.java index 839b921..a70eb2c 100644 --- a/android/src/main/java/com/stallion/storage/StallionConfigConstants.java +++ b/android/src/main/java/com/stallion/storage/StallionConfigConstants.java @@ -12,6 +12,7 @@ public class StallionConfigConstants { public static final String API_KEY_IDENTIFIER = "x-sdk-access-token"; public static final String LAST_DOWNLOADING_URL_IDENTIFIER = "StallionLastDownloadingUrl"; public static final String LAST_UNVERIFIED_HASH = "LastUnverifiedHash"; + public static final String BASE_URL_IDENTIFIER = "StallionBaseUrl"; public static final String PROD_DIRECTORY = "/StallionProd"; public static final String STAGE_DIRECTORY = "/StallionStage"; diff --git a/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java b/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java new file mode 100644 index 0000000..e3aa450 --- /dev/null +++ b/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java @@ -0,0 +1,41 @@ +package com.stallion.utils; + +import com.stallion.storage.StallionStateManager; +import com.stallion.networkmanager.StallionApiConstants; + +public class StallionApiBaseUrl { + + /** + * Gets the API base URL from config or returns default + * @return String - The base URL to use + */ + public static String get() { + try { + StallionStateManager stateManager = StallionStateManager.getInstance(); + if (stateManager != null && stateManager.getStallionConfig() != null) { + String customBaseUrl = stateManager.getStallionConfig().getBaseUrl(); + return (customBaseUrl != null && !customBaseUrl.isEmpty()) + ? customBaseUrl + : StallionApiConstants.DEFAULT_STALLION_API_BASE; + } + } catch (Exception e) { + // Fallback to default on any error + } + return StallionApiConstants.DEFAULT_STALLION_API_BASE; + } + + /** + * Sets a custom base URL + * @param baseUrl - The custom base URL to set + */ + public static void set(String baseUrl) { + try { + StallionStateManager stateManager = StallionStateManager.getInstance(); + if (stateManager != null && stateManager.getStallionConfig() != null) { + stateManager.getStallionConfig().setBaseUrl(baseUrl); + } + } catch (Exception e) { + // Silently fail + } + } +} diff --git a/example/android/app/src/main/res/values/strings.xml b/example/android/app/src/main/res/values/strings.xml index 1ae723b..6be6785 100644 --- a/example/android/app/src/main/res/values/strings.xml +++ b/example/android/app/src/main/res/values/strings.xml @@ -2,11 +2,4 @@ StallionExample 67210b6f3e19d894c6a1a4fb spb_KLNEjOkETM48i3N1SffGDvEl-OsSiHZrhxER2kq3Ok - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApm9SgaDU9lXLi5OD6sd9 -WpNP8bWeGRzLYOReqH5yuu21kYTDaDt1/+vTpCBNuyX2ZjtKNvKujnAXuWf8Zfj4 -JcWb/r4clDQK0sNy8G7x7s+4Mup6W73lW5AtUnREQZX8IA00lp76r9ii6HYxXTpO -CgHpXyRyPLkMNc+HxaTmDHbldZtRCREcLldI7NnQkFMyZlHK2r1QxiQLpTIEK0vI -IJyXeHOeJUROebZn/UI7UDKiSCvlaHMsJ4pnIAyLrtqFQ8w9rf3aljmYX52m6Rbc -df8fwbWtBCH8OPW5SEDTMDzvE1siAvYwRNxbeXHMrAypPg/DaATdXoKMObjJWRm1 -owIDAQAB diff --git a/example/ios/StallionExample.xcodeproj/xcshareddata/xcschemes/StallionExample.xcscheme b/example/ios/StallionExample.xcodeproj/xcshareddata/xcschemes/StallionExample.xcscheme index 3b26d10..b424e51 100644 --- a/example/ios/StallionExample.xcodeproj/xcshareddata/xcschemes/StallionExample.xcscheme +++ b/example/ios/StallionExample.xcodeproj/xcshareddata/xcschemes/StallionExample.xcscheme @@ -41,7 +41,7 @@ - StallionPublicSigningKey - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApm9SgaDU9lXLi5OD6sd9 -WpNP8bWeGRzLYOReqH5yuu21kYTDaDt1/+vTpCBNuyX2ZjtKNvKujnAXuWf8Zfj4 -JcWb/r4clDQK0sNy8G7x7s+4Mup6W73lW5AtUnREQZX8IA00lp76r9ii6HYxXTpO -CgHpXyRyPLkMNc+HxaTmDHbldZtRCREcLldI7NnQkFMyZlHK2r1QxiQLpTIEK0vI -IJyXeHOeJUROebZn/UI7UDKiSCvlaHMsJ4pnIAyLrtqFQ8w9rf3aljmYX52m6Rbc -df8fwbWtBCH8OPW5SEDTMDzvE1siAvYwRNxbeXHMrAypPg/DaATdXoKMObjJWRm1 -owIDAQAB StallionAppToken spb_qLFBKtdR9TBZtsKPDqMXvFD_ebcs-Tdjyc7F4-dX7q StallionProjectId diff --git a/example/src/App.tsx b/example/src/App.tsx index 221da64..02e34d9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,7 +44,9 @@ const App: React.FC = () => { ); }; -export default withStallion(App); +export default withStallion(App, { + baseUrl: 'https://api.stalliontech.io', +}); const styles = StyleSheet.create({ container: { diff --git a/ios/main/Stallion.swift b/ios/main/Stallion.swift index 15be93d..ba0c500 100644 --- a/ios/main/Stallion.swift +++ b/ios/main/Stallion.swift @@ -28,6 +28,16 @@ class Stallion: RCTEventEmitter { } @objc func onLaunch(_ launchData: String) { + // Parse launch data and update baseUrl if provided + if !launchData.isEmpty { + if let data = launchData.data(using: .utf8), + let params = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let baseUrl = params["baseUrl"] as? String, + !baseUrl.isEmpty { + StallionApiBaseUrl.set(baseUrl) + } + } + stallionStateManager.isMounted = true checkPendingDownloads() let currentReleaseHash = stallionStateManager.stallionMeta.getHashAtCurrentProdSlot() diff --git a/ios/main/StallionApiBaseUrl.swift b/ios/main/StallionApiBaseUrl.swift new file mode 100644 index 0000000..7162dd3 --- /dev/null +++ b/ios/main/StallionApiBaseUrl.swift @@ -0,0 +1,35 @@ +// +// StallionApiBaseUrl.swift +// react-native-stallion +// +// Created for centralized base URL management +// + +import Foundation + +class StallionApiBaseUrl { + /** + * Gets the API base URL from config or returns default + * @returns String - The base URL to use + */ + static func get() -> String { + guard let stateManager = StallionStateManager.sharedInstance() else { + return StallionConstants.DEFAULT_STALLION_API_BASE + } + + let customBaseUrl = stateManager.stallionConfig.baseUrl + return customBaseUrl?.isEmpty == false ? customBaseUrl! : StallionConstants.DEFAULT_STALLION_API_BASE + } + + /** + * Sets a custom base URL + * @param baseUrl - The custom base URL to set + */ + static func set(_ baseUrl: String) { + guard let stateManager = StallionStateManager.sharedInstance() else { + return + } + stateManager.stallionConfig.updateBaseUrl(baseUrl) + } +} + diff --git a/ios/main/StallionConfig.h b/ios/main/StallionConfig.h index 46adde2..7813598 100644 --- a/ios/main/StallionConfig.h +++ b/ios/main/StallionConfig.h @@ -17,11 +17,13 @@ @property (nonatomic, copy, readonly) NSString *filesDirectory; @property (nonatomic, copy, readonly) NSString *publicSigningKey; @property (nonatomic, copy) NSString *lastUnverifiedHash; +@property (nonatomic, copy) NSString *baseUrl; - (instancetype)initWithDefaults:(NSUserDefaults *)defaults; - (void)updateSdkToken:(NSString *)newSdkToken; - (void)updateLastUnverifiedHash:(NSString *)newUnverifiedHash; +- (void)updateBaseUrl:(NSString *)newBaseUrl; - (NSDictionary *)toDictionary; @end diff --git a/ios/main/StallionConfig.m b/ios/main/StallionConfig.m index e75c9a0..f95cce6 100644 --- a/ios/main/StallionConfig.m +++ b/ios/main/StallionConfig.m @@ -7,6 +7,7 @@ #import "StallionConfig.h" #import "StallionConfigConstants.h" +#import "StallionObjConstants.h" @implementation StallionConfig @@ -20,6 +21,7 @@ - (instancetype)initWithDefaults:(NSUserDefaults *)defaults { _filesDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject ?: @""; _publicSigningKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER] ?: @""; _lastUnverifiedHash = [defaults stringForKey:LAST_UNVERIFIED_KEY_IDENTIFIER] ?: @""; + _baseUrl = [defaults stringForKey:BASE_URL_IDENTIFIER] ?: [StallionObjConstants default_stallion_api_base]; NSString *cachedUid = [defaults stringForKey:UNIQUE_ID_IDENTIFIER]; if (cachedUid && ![cachedUid isEqualToString:@""]) { @@ -51,6 +53,12 @@ - (void)updateLastUnverifiedHash:(NSString *)newUnverifiedHash { [[NSUserDefaults standardUserDefaults] synchronize]; } +- (void)updateBaseUrl:(NSString *)newBaseUrl { + _baseUrl = newBaseUrl ?: [StallionObjConstants default_stallion_api_base]; + [[NSUserDefaults standardUserDefaults] setObject:_baseUrl forKey:BASE_URL_IDENTIFIER]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + - (NSDictionary *)toDictionary { @try { return @{ @@ -58,7 +66,8 @@ - (NSDictionary *)toDictionary { @"projectId": self.projectId ?: @"", @"appToken": self.appToken ?: @"", @"sdkToken": self.sdkToken ?: @"", - @"appVersion": self.appVersion ?: @"" + @"appVersion": self.appVersion ?: @"", + @"baseUrl": self.baseUrl ?: [StallionObjConstants default_stallion_api_base] }; } @catch (NSException *exception) { NSLog(@"Error in toDictionary: %@", exception.reason); diff --git a/ios/main/StallionConfigConstants.h b/ios/main/StallionConfigConstants.h index 7c0d921..7ba39dc 100644 --- a/ios/main/StallionConfigConstants.h +++ b/ios/main/StallionConfigConstants.h @@ -32,5 +32,6 @@ extern NSString *const STALLION_META_IDENTIFIER; extern NSString *const STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER; extern NSString *const LAST_UNVERIFIED_KEY_IDENTIFIER; +extern NSString *const BASE_URL_IDENTIFIER; @end diff --git a/ios/main/StallionConfigConstants.m b/ios/main/StallionConfigConstants.m index 3ab91af..1303431 100644 --- a/ios/main/StallionConfigConstants.m +++ b/ios/main/StallionConfigConstants.m @@ -32,5 +32,6 @@ @implementation StallionConfigConstants NSString *const STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER = @"StallionPublicSigningKey"; NSString *const LAST_UNVERIFIED_KEY_IDENTIFIER = @"LastUnverifiedHash"; +NSString *const BASE_URL_IDENTIFIER = @"StallionBaseUrl"; @end diff --git a/ios/main/StallionConstants.swift b/ios/main/StallionConstants.swift index 4a736d0..af18687 100644 --- a/ios/main/StallionConstants.swift +++ b/ios/main/StallionConstants.swift @@ -45,7 +45,12 @@ class StallionConstants { static let CURRENT_PROD_SLOT_KEY = "stallionProdCurrentSlot" static let CURRENT_STAGE_SLOT_KEY = "stallionStageCurrentSlot" - static let STALLION_API_BASE = "https://api.stalliontech.io" + // Use utility for base URL (returns custom or default) + static var STALLION_API_BASE: String { + return StallionApiBaseUrl.get() + } + // Keep default as constant for reference + static let DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io" static let STALLION_INFO_API_PATH = "/api/v1/promoted/get-update-meta" static let STALLION_PROJECT_ID_IDENTIFIER = "StallionProjectId" static let STALLION_APP_TOKEN_IDENTIFIER = "StallionAppToken" diff --git a/ios/main/StallionObjConstants.h b/ios/main/StallionObjConstants.h index e57ae28..d9b6373 100644 --- a/ios/main/StallionObjConstants.h +++ b/ios/main/StallionObjConstants.h @@ -45,5 +45,6 @@ @property (class, nonatomic, readonly) NSString *last_rolled_back_release_hash_key; @property (class, nonatomic, readonly) NSString *auto_rolled_back_prod_event; @property (class, nonatomic, readonly) NSString *is_auto_rollback_key; +@property (class, nonatomic, readonly) NSString *default_stallion_api_base; @end diff --git a/ios/main/StallionObjConstants.m b/ios/main/StallionObjConstants.m index ed6f9f7..e624b4a 100644 --- a/ios/main/StallionObjConstants.m +++ b/ios/main/StallionObjConstants.m @@ -129,4 +129,8 @@ + (NSString *)last_rolled_back_release_hash_key { return @"LAST_ROLLED_BACK_RELEASE_HASH"; } ++ (NSString *)default_stallion_api_base { + return @"https://api.stalliontech.io"; +} + @end diff --git a/src/main/constants/apiConstants.ts b/src/main/constants/apiConstants.ts index b89a4ae..b664ebc 100644 --- a/src/main/constants/apiConstants.ts +++ b/src/main/constants/apiConstants.ts @@ -1,4 +1,8 @@ -export const API_BASE_URL = 'https://api.stalliontech.io'; +import { DEFAULT_API_BASE_URL } from '../utils/getApiBaseUrl'; + +// Keep for backward compatibility, but mark as deprecated +/** @deprecated Use getApiBaseUrl() instead */ +export const API_BASE_URL = DEFAULT_API_BASE_URL; export enum API_PATHS { LOGIN = '/api/v1/sdk/auth/verify-pin', diff --git a/src/main/state/actionCreators/useConfigActions.ts b/src/main/state/actionCreators/useConfigActions.ts index 13974aa..f8a223a 100644 --- a/src/main/state/actionCreators/useConfigActions.ts +++ b/src/main/state/actionCreators/useConfigActions.ts @@ -1,6 +1,7 @@ import React, { useCallback, useEffect } from 'react'; import { getStallionConfigNative } from '../../utils/StallionNativeUtils'; +import { clearApiBaseUrlCache } from '../../utils/getApiBaseUrl'; import { IConfigAction } from '../../../types/config.types'; import { setConfig } from '../actions/configActions'; @@ -10,6 +11,8 @@ const useConfigActions = (dispatch: React.Dispatch) => { try { const stallionConfig = await getStallionConfigNative(); dispatch(setConfig(stallionConfig)); + // Clear base URL cache when config is refreshed to pick up any baseUrl changes + clearApiBaseUrlCache(); } catch (_) {} }, [dispatch]); diff --git a/src/main/utils/getApiBaseUrl.ts b/src/main/utils/getApiBaseUrl.ts new file mode 100644 index 0000000..0f841f6 --- /dev/null +++ b/src/main/utils/getApiBaseUrl.ts @@ -0,0 +1,41 @@ +import { getStallionConfigNative } from './StallionNativeUtils'; + +// Default constant +export const DEFAULT_API_BASE_URL = 'https://api.stalliontech.io'; + +// Cache for base URL +let cachedBaseUrl: string | null = null; + +/** + * Gets the API base URL from native config or returns default + * @returns Promise - The base URL to use + */ +export const getApiBaseUrl = async (): Promise => { + if (cachedBaseUrl) { + return cachedBaseUrl; + } + + try { + const config = await getStallionConfigNative(); + cachedBaseUrl = config.baseUrl || DEFAULT_API_BASE_URL; + return cachedBaseUrl; + } catch { + return DEFAULT_API_BASE_URL; + } +}; + +/** + * Gets the API base URL synchronously (returns default if not cached) + * Use this when you need immediate access and can't await + * @returns string - The base URL (default if not loaded yet) + */ +export const getApiBaseUrlSync = (): string => { + return cachedBaseUrl || DEFAULT_API_BASE_URL; +}; + +/** + * Clears the cached base URL (useful for testing or config changes) + */ +export const clearApiBaseUrlCache = (): void => { + cachedBaseUrl = null; +}; diff --git a/src/main/utils/useApiClient.ts b/src/main/utils/useApiClient.ts index afa5ad0..c151ef2 100644 --- a/src/main/utils/useApiClient.ts +++ b/src/main/utils/useApiClient.ts @@ -1,5 +1,6 @@ -import { useCallback } from 'react'; -import { API_BASE_URL, API_PATHS } from '../constants/apiConstants'; +import { useCallback, useState, useEffect } from 'react'; +import { getApiBaseUrl, DEFAULT_API_BASE_URL } from './getApiBaseUrl'; +import { API_PATHS } from '../constants/apiConstants'; import { IStallionConfigJson } from '../../types/config.types'; type IAuthHandler = (loginRequired: boolean) => void; @@ -8,9 +9,16 @@ export const useApiClient = ( configState: IStallionConfigJson, authHandler?: IAuthHandler ) => { + const [baseUrl, setBaseUrl] = useState(DEFAULT_API_BASE_URL); + + // Load base URL on mount + useEffect(() => { + getApiBaseUrl().then(setBaseUrl); + }, []); + const getData = useCallback( (apiPath: API_PATHS, apiBody: object): Promise => { - const dataRequest = fetch(API_BASE_URL + apiPath, { + const dataRequest = fetch(baseUrl + apiPath, { method: 'POST', body: JSON.stringify(apiBody), headers: { @@ -29,7 +37,7 @@ export const useApiClient = ( }); } else return dataRequest.then((res) => res.json()); }, - [configState, authHandler] + [baseUrl, configState, authHandler] ); return { getData, diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 0657558..aa9f98e 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -4,6 +4,7 @@ export interface IStallionConfigJson { appToken: string; sdkToken: string; appVersion: string; + baseUrl?: string; } export enum ConfigActionKind { diff --git a/src/types/utils.types.ts b/src/types/utils.types.ts index 23e36eb..e8251f7 100644 --- a/src/types/utils.types.ts +++ b/src/types/utils.types.ts @@ -8,7 +8,9 @@ interface IBundleInfo { hash: string; } -export interface IStallionInitParams {} +export interface IStallionInitParams { + baseUrl?: string; // Optional, defaults to 'https://api.stalliontech.io' +} export type IWithStallion = ( BaseComponent: React.ComponentType, From d302ababe44bf1a7faa4f212835189efc12a39f0 Mon Sep 17 00:00:00 2001 From: Thor963 Date: Sun, 31 May 2026 00:01:26 +0530 Subject: [PATCH 2/2] chore: added regionalisation support --- .../java/com/stallion/StallionModule.java | 10 ++-- .../networkmanager/StallionApiConstants.java | 11 +++- .../com/stallion/storage/StallionConfig.java | 20 ++++--- .../stallion/utils/StallionApiBaseUrl.java | 36 +++++++++---- .../stallion/utils/StallionTokenRegion.java | 51 ++++++++++++++++++ example/src/App.tsx | 4 +- ios/main/Stallion.swift | 23 ++++---- ios/main/StallionApiBaseUrl.swift | 36 +++++++++---- ios/main/StallionConfig.m | 13 +++-- ios/main/StallionConstants.swift | 8 ++- ios/main/StallionTokenRegion.swift | 44 +++++++++++++++ package.json | 2 +- src/__tests__/parseTokenRegion.test.ts | 34 ++++++++++++ .../modules/listing/components/SlotView.tsx | 2 +- src/main/state/useStallionEvents.ts | 2 + src/main/utils/getApiBaseUrl.ts | 38 ++++++++----- src/main/utils/parseTokenRegion.ts | 53 +++++++++++++++++++ src/types/utils.types.ts | 2 +- 18 files changed, 322 insertions(+), 67 deletions(-) create mode 100644 android/src/main/java/com/stallion/utils/StallionTokenRegion.java create mode 100644 ios/main/StallionTokenRegion.swift create mode 100644 src/__tests__/parseTokenRegion.test.ts create mode 100644 src/main/utils/parseTokenRegion.ts diff --git a/android/src/main/java/com/stallion/StallionModule.java b/android/src/main/java/com/stallion/StallionModule.java index 05027b9..f3bf281 100644 --- a/android/src/main/java/com/stallion/StallionModule.java +++ b/android/src/main/java/com/stallion/StallionModule.java @@ -62,15 +62,17 @@ public String getName() { @ReactMethod public void onLaunch(String launchData) { try { + String customBaseUrl = null; if (launchData != null && !launchData.isEmpty()) { JSONObject launchDataJson = new JSONObject(launchData); - if (launchDataJson.has("baseUrl")) { - String baseUrl = launchDataJson.getString("baseUrl"); - if (baseUrl != null && !baseUrl.isEmpty()) { - com.stallion.utils.StallionApiBaseUrl.set(baseUrl); + if (launchDataJson.has("baseUrl") && !launchDataJson.isNull("baseUrl")) { + String baseUrl = launchDataJson.optString("baseUrl", ""); + if (!baseUrl.isEmpty()) { + customBaseUrl = baseUrl; } } } + com.stallion.utils.StallionApiBaseUrl.set(customBaseUrl); } catch (Exception e) { e.printStackTrace(); } diff --git a/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java b/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java index 51e92c4..8cbb565 100644 --- a/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java +++ b/android/src/main/java/com/stallion/networkmanager/StallionApiConstants.java @@ -22,8 +22,15 @@ public class StallionApiConstants { public static final String STALLION_SDK_TOKEN_KEY = "x-sdk-pin-access-token"; public static final String STALLION_DEVICE_ID_KEY = "uid"; - // Default constant for reference - public static final String DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io"; + /** Legacy global API host; used only to detect implicit defaults in prefs. */ + public static final String LEGACY_DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io"; + + public static final String REGIONAL_API_BASE_AP = "https://api-ap.stalliontech.io"; + public static final String REGIONAL_API_BASE_US = "https://api-us.stalliontech.io"; + + /** @deprecated Use LEGACY_DEFAULT_STALLION_API_BASE or getStallionApiBase() */ + @Deprecated + public static final String DEFAULT_STALLION_API_BASE = LEGACY_DEFAULT_STALLION_API_BASE; /** * Gets the API base URL from config or returns default diff --git a/android/src/main/java/com/stallion/storage/StallionConfig.java b/android/src/main/java/com/stallion/storage/StallionConfig.java index d71e4ac..2b48e3b 100644 --- a/android/src/main/java/com/stallion/storage/StallionConfig.java +++ b/android/src/main/java/com/stallion/storage/StallionConfig.java @@ -12,7 +12,7 @@ import java.util.UUID; import com.stallion.storage.StallionConfigConstants; -import com.stallion.networkmanager.StallionApiConstants; +import com.stallion.utils.StallionApiBaseUrl; public class StallionConfig { private String uid; @@ -27,7 +27,6 @@ public class StallionConfig { private final String publicSigningKey; private String baseUrl; - public StallionConfig(Context context, SharedPreferences sharedPreferences) { this.sharedPreferences = sharedPreferences; Resources res = context.getResources(); @@ -81,7 +80,8 @@ public StallionConfig(Context context, SharedPreferences sharedPreferences) { this.filesDirectory = context.getFilesDir().getAbsolutePath(); this.lastDownloadingUrl = sharedPreferences.getString(StallionConfigConstants.LAST_DOWNLOADING_URL_IDENTIFIER, ""); this.lastUnverifiedHash = sharedPreferences.getString(StallionConfigConstants.LAST_UNVERIFIED_HASH, ""); - this.baseUrl = sharedPreferences.getString(StallionConfigConstants.BASE_URL_IDENTIFIER, StallionApiConstants.DEFAULT_STALLION_API_BASE); + String storedBaseUrl = sharedPreferences.getString(StallionConfigConstants.BASE_URL_IDENTIFIER, ""); + this.baseUrl = storedBaseUrl != null ? storedBaseUrl : ""; } public String getLastDownloadingUrl() { @@ -153,8 +153,16 @@ public String getBaseUrl() { } public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl != null ? baseUrl : StallionApiConstants.DEFAULT_STALLION_API_BASE; - sharedPreferences.edit().putString(StallionConfigConstants.BASE_URL_IDENTIFIER, this.baseUrl).apply(); + if (baseUrl == null || baseUrl.isEmpty()) { + this.baseUrl = ""; + sharedPreferences.edit().remove(StallionConfigConstants.BASE_URL_IDENTIFIER).apply(); + return; + } + this.baseUrl = baseUrl; + sharedPreferences + .edit() + .putString(StallionConfigConstants.BASE_URL_IDENTIFIER, this.baseUrl) + .apply(); } public JSONObject toJSON() { @@ -165,7 +173,7 @@ public JSONObject toJSON() { configJson.put("appToken", this.appToken); configJson.put("sdkToken", this.sdkToken); configJson.put("appVersion", this.appVersion); - configJson.put("baseUrl", this.baseUrl != null ? this.baseUrl : StallionApiConstants.DEFAULT_STALLION_API_BASE); + configJson.put("baseUrl", StallionApiBaseUrl.get()); return configJson; } catch (JSONException ignored) { return new JSONObject(); diff --git a/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java b/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java index e3aa450..e17d2a7 100644 --- a/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java +++ b/android/src/main/java/com/stallion/utils/StallionApiBaseUrl.java @@ -1,32 +1,50 @@ package com.stallion.utils; +import com.stallion.storage.StallionConfig; import com.stallion.storage.StallionStateManager; import com.stallion.networkmanager.StallionApiConstants; public class StallionApiBaseUrl { /** - * Gets the API base URL from config or returns default + * Gets the API base URL: custom baseUrl if set, else regional URL from app token. * @return String - The base URL to use */ public static String get() { try { StallionStateManager stateManager = StallionStateManager.getInstance(); if (stateManager != null && stateManager.getStallionConfig() != null) { - String customBaseUrl = stateManager.getStallionConfig().getBaseUrl(); - return (customBaseUrl != null && !customBaseUrl.isEmpty()) - ? customBaseUrl - : StallionApiConstants.DEFAULT_STALLION_API_BASE; + return resolve(stateManager.getStallionConfig()); } } catch (Exception e) { - // Fallback to default on any error + // Fallback to regional default on any error } - return StallionApiConstants.DEFAULT_STALLION_API_BASE; + return StallionApiConstants.REGIONAL_API_BASE_AP; + } + + static String resolve(StallionConfig config) { + String stored = config.getBaseUrl(); + if (stored != null && !stored.isEmpty()) { + return stored; + } + + String region = StallionTokenRegion.parseTokenRegion(config.getAppToken()); + if (region == null) { + region = StallionTokenRegion.defaultRegion(); + } + return regionalBaseUrl(region); + } + + static String regionalBaseUrl(String region) { + if ("us".equals(region)) { + return StallionApiConstants.REGIONAL_API_BASE_US; + } + return StallionApiConstants.REGIONAL_API_BASE_AP; } /** - * Sets a custom base URL - * @param baseUrl - The custom base URL to set + * Sets a custom base URL, or clears it when null/empty. + * @param baseUrl - The custom base URL to set, or null/empty to clear */ public static void set(String baseUrl) { try { diff --git a/android/src/main/java/com/stallion/utils/StallionTokenRegion.java b/android/src/main/java/com/stallion/utils/StallionTokenRegion.java new file mode 100644 index 0000000..34667e8 --- /dev/null +++ b/android/src/main/java/com/stallion/utils/StallionTokenRegion.java @@ -0,0 +1,51 @@ +package com.stallion.utils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +public final class StallionTokenRegion { + + private static final Set VALID_REGIONS = new HashSet<>(Arrays.asList("ap", "us")); + + private StallionTokenRegion() {} + + /** + * Parses the region prefix from a Stallion token. + * Returns "ap", "us", or null. Null means legacy unprefixed token; caller should default to "ap". + */ + public static String parseTokenRegion(String token) { + if (token == null) { + return null; + } + token = token.trim(); + if (token.isEmpty()) { + return null; + } + + // App token: spb__<44-char nanoid> → 49 chars + if (token.startsWith("spb_") && token.length() == 49 && token.charAt(6) == '_') { + return extractRegion(token); + } + + // CI token: stl__<36-char nanoid> → 43 chars + if (token.startsWith("stl_") && token.length() == 43 && token.charAt(6) == '_') { + return extractRegion(token); + } + + return null; + } + + public static String defaultRegion() { + return "ap"; + } + + private static String extractRegion(String token) { + String code = token.substring(4, 6).toLowerCase(Locale.ROOT); + if (!code.matches("[a-z]{2}")) { + return null; + } + return VALID_REGIONS.contains(code) ? code : null; + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx index 02e34d9..221da64 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,9 +44,7 @@ const App: React.FC = () => { ); }; -export default withStallion(App, { - baseUrl: 'https://api.stalliontech.io', -}); +export default withStallion(App); const styles = StyleSheet.create({ container: { diff --git a/ios/main/Stallion.swift b/ios/main/Stallion.swift index ba0c500..6fbb7f1 100644 --- a/ios/main/Stallion.swift +++ b/ios/main/Stallion.swift @@ -28,16 +28,17 @@ class Stallion: RCTEventEmitter { } @objc func onLaunch(_ launchData: String) { - // Parse launch data and update baseUrl if provided - if !launchData.isEmpty { - if let data = launchData.data(using: .utf8), - let params = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let baseUrl = params["baseUrl"] as? String, - !baseUrl.isEmpty { - StallionApiBaseUrl.set(baseUrl) - } + var customBaseUrl = "" + if !launchData.isEmpty, + let data = launchData.data(using: .utf8), + let params = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + params.keys.contains("baseUrl"), + let baseUrl = params["baseUrl"] as? String, + !baseUrl.isEmpty { + customBaseUrl = baseUrl } - + StallionApiBaseUrl.set(customBaseUrl) + stallionStateManager.isMounted = true checkPendingDownloads() let currentReleaseHash = stallionStateManager.stallionMeta.getHashAtCurrentProdSlot() @@ -71,7 +72,9 @@ class Stallion: RCTEventEmitter { @objc func getStallionConfig(_ promise: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { do { if let config = stallionStateManager.stallionConfig { - let configJsonData = try JSONSerialization.data(withJSONObject: config.toDictionary(), options: []) + var configDict = config.toDictionary() as? [String: Any] ?? [:] + configDict["baseUrl"] = StallionApiBaseUrl.get() + let configJsonData = try JSONSerialization.data(withJSONObject: configDict, options: []) if let configJsonString = String(data: configJsonData, encoding: .utf8) { promise(configJsonString) } else { diff --git a/ios/main/StallionApiBaseUrl.swift b/ios/main/StallionApiBaseUrl.swift index 7162dd3..bd9c9bc 100644 --- a/ios/main/StallionApiBaseUrl.swift +++ b/ios/main/StallionApiBaseUrl.swift @@ -9,21 +9,38 @@ import Foundation class StallionApiBaseUrl { /** - * Gets the API base URL from config or returns default - * @returns String - The base URL to use + * Gets the API base URL: custom baseUrl if set, else regional URL from app token. */ static func get() -> String { guard let stateManager = StallionStateManager.sharedInstance() else { - return StallionConstants.DEFAULT_STALLION_API_BASE + return StallionConstants.REGIONAL_API_BASE_AP } - - let customBaseUrl = stateManager.stallionConfig.baseUrl - return customBaseUrl?.isEmpty == false ? customBaseUrl! : StallionConstants.DEFAULT_STALLION_API_BASE + + return resolve(config: stateManager.stallionConfig) + } + + static func resolve(config: StallionConfig) -> String { + let stored = config.baseUrl ?? "" + if !stored.isEmpty { + return stored + } + + var region = StallionTokenRegion.parseTokenRegion(config.appToken) + if region == nil { + region = StallionTokenRegion.defaultRegion() + } + return regionalBaseUrl(region!) } - + + static func regionalBaseUrl(_ region: String) -> String { + if region == "us" { + return StallionConstants.REGIONAL_API_BASE_US + } + return StallionConstants.REGIONAL_API_BASE_AP + } + /** - * Sets a custom base URL - * @param baseUrl - The custom base URL to set + * Sets a custom base URL, or clears it when empty. */ static func set(_ baseUrl: String) { guard let stateManager = StallionStateManager.sharedInstance() else { @@ -32,4 +49,3 @@ class StallionApiBaseUrl { stateManager.stallionConfig.updateBaseUrl(baseUrl) } } - diff --git a/ios/main/StallionConfig.m b/ios/main/StallionConfig.m index f95cce6..d96eb44 100644 --- a/ios/main/StallionConfig.m +++ b/ios/main/StallionConfig.m @@ -21,7 +21,7 @@ - (instancetype)initWithDefaults:(NSUserDefaults *)defaults { _filesDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject ?: @""; _publicSigningKey = [[NSBundle mainBundle] objectForInfoDictionaryKey:STALLION_PUBLIC_SIGNING_KEY_IDENTIFIER] ?: @""; _lastUnverifiedHash = [defaults stringForKey:LAST_UNVERIFIED_KEY_IDENTIFIER] ?: @""; - _baseUrl = [defaults stringForKey:BASE_URL_IDENTIFIER] ?: [StallionObjConstants default_stallion_api_base]; + _baseUrl = [defaults stringForKey:BASE_URL_IDENTIFIER] ?: @""; NSString *cachedUid = [defaults stringForKey:UNIQUE_ID_IDENTIFIER]; if (cachedUid && ![cachedUid isEqualToString:@""]) { @@ -54,8 +54,13 @@ - (void)updateLastUnverifiedHash:(NSString *)newUnverifiedHash { } - (void)updateBaseUrl:(NSString *)newBaseUrl { - _baseUrl = newBaseUrl ?: [StallionObjConstants default_stallion_api_base]; - [[NSUserDefaults standardUserDefaults] setObject:_baseUrl forKey:BASE_URL_IDENTIFIER]; + if (newBaseUrl == nil || [newBaseUrl length] == 0) { + _baseUrl = @""; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:BASE_URL_IDENTIFIER]; + } else { + _baseUrl = newBaseUrl; + [[NSUserDefaults standardUserDefaults] setObject:_baseUrl forKey:BASE_URL_IDENTIFIER]; + } [[NSUserDefaults standardUserDefaults] synchronize]; } @@ -67,7 +72,7 @@ - (NSDictionary *)toDictionary { @"appToken": self.appToken ?: @"", @"sdkToken": self.sdkToken ?: @"", @"appVersion": self.appVersion ?: @"", - @"baseUrl": self.baseUrl ?: [StallionObjConstants default_stallion_api_base] + @"baseUrl": self.baseUrl ?: @"" }; } @catch (NSException *exception) { NSLog(@"Error in toDictionary: %@", exception.reason); diff --git a/ios/main/StallionConstants.swift b/ios/main/StallionConstants.swift index af18687..8c55183 100644 --- a/ios/main/StallionConstants.swift +++ b/ios/main/StallionConstants.swift @@ -49,8 +49,12 @@ class StallionConstants { static var STALLION_API_BASE: String { return StallionApiBaseUrl.get() } - // Keep default as constant for reference - static let DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io" + /// Legacy global API host; used only to detect implicit defaults in prefs. + static let LEGACY_DEFAULT_STALLION_API_BASE = "https://api.stalliontech.io" + static let REGIONAL_API_BASE_AP = "https://api-ap.stalliontech.io" + static let REGIONAL_API_BASE_US = "https://api-us.stalliontech.io" + /// @deprecated Use LEGACY_DEFAULT_STALLION_API_BASE or StallionApiBaseUrl.get() + static let DEFAULT_STALLION_API_BASE = LEGACY_DEFAULT_STALLION_API_BASE static let STALLION_INFO_API_PATH = "/api/v1/promoted/get-update-meta" static let STALLION_PROJECT_ID_IDENTIFIER = "StallionProjectId" static let STALLION_APP_TOKEN_IDENTIFIER = "StallionAppToken" diff --git a/ios/main/StallionTokenRegion.swift b/ios/main/StallionTokenRegion.swift new file mode 100644 index 0000000..3a8c69c --- /dev/null +++ b/ios/main/StallionTokenRegion.swift @@ -0,0 +1,44 @@ +// +// StallionTokenRegion.swift +// react-native-stallion +// + +import Foundation + +enum StallionTokenRegion { + private static let validRegions: Set = ["ap", "us"] + + /// Parses the region prefix from a Stallion token. + /// Returns "ap", "us", or nil. Nil means legacy unprefixed token; caller should default to "ap". + static func parseTokenRegion(_ token: String?) -> String? { + guard let token = token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty else { + return nil + } + + // App token: spb__<44-char nanoid> → 49 chars + if token.hasPrefix("spb_"), token.count == 49, token[token.index(token.startIndex, offsetBy: 6)] == "_" { + return extractRegion(from: token) + } + + // CI token: stl__<36-char nanoid> → 43 chars + if token.hasPrefix("stl_"), token.count == 43, token[token.index(token.startIndex, offsetBy: 6)] == "_" { + return extractRegion(from: token) + } + + return nil + } + + static func defaultRegion() -> String { + return "ap" + } + + private static func extractRegion(from token: String) -> String? { + let start = token.index(token.startIndex, offsetBy: 4) + let end = token.index(token.startIndex, offsetBy: 6) + let code = String(token[start.. { + it('parses stl_ap CI token', () => { + expect( + parseTokenRegion('stl_ap_LaTCO8IcJYf6Ga1fFrNjLGfjko9HTlKbaYTS') + ).toBe('ap'); + }); + + it('parses stl_us CI token', () => { + expect(parseTokenRegion('stl_us_' + 'a'.repeat(36))).toBe('us'); + }); + + it('returns null for legacy spb token without region', () => { + expect( + parseTokenRegion('spb_qLFBKtdR9TBZtsKPDqMXvFD_ebcs-Tdjyc7F4-dX7q') + ).toBeNull(); + }); + + it('returns null for invalid region code', () => { + const invalid = 'stl_xx_' + 'a'.repeat(36); + expect(parseTokenRegion(invalid)).toBeNull(); + }); + + it('defaults regional URL to AP when region is null', () => { + expect(regionalApiBaseUrl(null)).toBe(AP_ORIGIN); + expect(regionalApiBaseUrl('us')).toBe(US_ORIGIN); + }); +}); diff --git a/src/main/components/modules/listing/components/SlotView.tsx b/src/main/components/modules/listing/components/SlotView.tsx index f01c940..d070fe6 100644 --- a/src/main/components/modules/listing/components/SlotView.tsx +++ b/src/main/components/modules/listing/components/SlotView.tsx @@ -45,7 +45,7 @@ const StallionSlot: React.FC = ({ slot, isActive }) => { borderWidth: isActive ? 1 : 0, }, ]} - > + /> ); }; diff --git a/src/main/state/useStallionEvents.ts b/src/main/state/useStallionEvents.ts index 0ed72f1..3cc12e7 100644 --- a/src/main/state/useStallionEvents.ts +++ b/src/main/state/useStallionEvents.ts @@ -19,6 +19,7 @@ import { useApiClient } from '../utils/useApiClient'; import { API_PATHS } from '../constants/apiConstants'; import debounce from '../utils/debounce'; import { IStallionInitParams } from '../../types/utils.types'; +import { clearApiBaseUrlCache } from '../utils/getApiBaseUrl'; import { IUpdateMetaAction, UpdateMetaActionKind, @@ -160,6 +161,7 @@ export const useStallionEvents = ( ); return; } + clearApiBaseUrlCache(); if (stallionInitParams) { try { onLaunchNative(JSON.stringify(stallionInitParams)); diff --git a/src/main/utils/getApiBaseUrl.ts b/src/main/utils/getApiBaseUrl.ts index 0f841f6..8e5476d 100644 --- a/src/main/utils/getApiBaseUrl.ts +++ b/src/main/utils/getApiBaseUrl.ts @@ -1,27 +1,37 @@ import { getStallionConfigNative } from './StallionNativeUtils'; +import { + AP_ORIGIN, + parseTokenRegion, + regionalApiBaseUrl, +} from './parseTokenRegion'; -// Default constant -export const DEFAULT_API_BASE_URL = 'https://api.stalliontech.io'; +export const DEFAULT_API_BASE_URL = AP_ORIGIN; +export const LEGACY_API_BASE_URL = 'https://api.stalliontech.io'; // Cache for base URL let cachedBaseUrl: string | null = null; /** - * Gets the API base URL from native config or returns default + * Gets the API base URL from native config or returns regional default * @returns Promise - The base URL to use */ export const getApiBaseUrl = async (): Promise => { - if (cachedBaseUrl) { - return cachedBaseUrl; - } + if (cachedBaseUrl) { + return cachedBaseUrl; + } - try { - const config = await getStallionConfigNative(); - cachedBaseUrl = config.baseUrl || DEFAULT_API_BASE_URL; - return cachedBaseUrl; - } catch { - return DEFAULT_API_BASE_URL; + try { + const config = await getStallionConfigNative(); + if (config.baseUrl) { + cachedBaseUrl = config.baseUrl; + return cachedBaseUrl; } + const region = parseTokenRegion(config.appToken); + cachedBaseUrl = regionalApiBaseUrl(region); + return cachedBaseUrl; + } catch { + return DEFAULT_API_BASE_URL; + } }; /** @@ -30,12 +40,12 @@ export const getApiBaseUrl = async (): Promise => { * @returns string - The base URL (default if not loaded yet) */ export const getApiBaseUrlSync = (): string => { - return cachedBaseUrl || DEFAULT_API_BASE_URL; + return cachedBaseUrl || DEFAULT_API_BASE_URL; }; /** * Clears the cached base URL (useful for testing or config changes) */ export const clearApiBaseUrlCache = (): void => { - cachedBaseUrl = null; + cachedBaseUrl = null; }; diff --git a/src/main/utils/parseTokenRegion.ts b/src/main/utils/parseTokenRegion.ts new file mode 100644 index 0000000..89f0c8b --- /dev/null +++ b/src/main/utils/parseTokenRegion.ts @@ -0,0 +1,53 @@ +const AP_ORIGIN = 'https://api-ap.stalliontech.io'; +const US_ORIGIN = 'https://api-us.stalliontech.io'; + +const VALID_REGIONS = new Set(['ap', 'us']); + +export { AP_ORIGIN, US_ORIGIN, VALID_REGIONS }; + +/** + * Parse the region prefix from a Stallion token. + * Returns 'ap' | 'us' | null. Null means legacy unprefixed token; default to 'ap'. + */ +export function parseTokenRegion(token: unknown): 'ap' | 'us' | null { + if (typeof token !== 'string') { + return null; + } + + const trimmed = token.trim(); + if (!trimmed) { + return null; + } + + // App token: spb__<44-char nanoid> → 49 chars + if ( + trimmed.startsWith('spb_') && + trimmed.length === 49 && + trimmed.charAt(6) === '_' + ) { + return extractRegion(trimmed); + } + + // CI token: stl__<36-char nanoid> → 43 chars + if ( + trimmed.startsWith('stl_') && + trimmed.length === 43 && + trimmed.charAt(6) === '_' + ) { + return extractRegion(trimmed); + } + + return null; +} + +function extractRegion(token: string): 'ap' | 'us' | null { + const code = token.substring(4, 6).toLowerCase(); + if (!/^[a-z]{2}$/.test(code)) { + return null; + } + return VALID_REGIONS.has(code) ? (code as 'ap' | 'us') : null; +} + +export function regionalApiBaseUrl(region: string | null): string { + return region === 'us' ? US_ORIGIN : AP_ORIGIN; +} diff --git a/src/types/utils.types.ts b/src/types/utils.types.ts index e8251f7..5c703a2 100644 --- a/src/types/utils.types.ts +++ b/src/types/utils.types.ts @@ -9,7 +9,7 @@ interface IBundleInfo { } export interface IStallionInitParams { - baseUrl?: string; // Optional, defaults to 'https://api.stalliontech.io' + baseUrl?: string; // Optional explicit override; otherwise derived from app token region (default AP) } export type IWithStallion = (