Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
implementation 'com.smallcase.gateway:sdk:6.1.1'
implementation 'com.smallcase.loans:sdk:5.1.3'
implementation 'com.smallcase.loans:sdk-sourav-native-dark-theme-82ee39c:5.1.4-91-release'
implementation "androidx.core:core-ktx:1.3.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
promise.reject(Throwable("Interaction token is null"))
return
}
val loanConfigObj = ScLoanInfo(interactionToken)
val loanConfigObj = ScLoanInfo(interactionToken, parseColorScheme(hashMap["colorScheme"]))
ScLoan.apply(appCompatActivity, loanConfigObj, object : ScLoanResult {
override fun onFailure(error: ScLoanError) {
promise.reject("${error.code}", scLoanResponseToWritableMap(error) ?: return)
Expand All @@ -376,7 +376,7 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
promise.reject(Throwable("Interaction token is null"))
return
}
val loanConfigObj = ScLoanInfo(interactionToken)
val loanConfigObj = ScLoanInfo(interactionToken, parseColorScheme(hashMap["colorScheme"]))
ScLoan.pay(appCompatActivity, loanConfigObj, object : ScLoanResult {
override fun onFailure(error: ScLoanError) {
promise.reject("${error.code}", scLoanResponseToWritableMap(error) ?: return)
Expand All @@ -400,7 +400,7 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
promise.reject(Throwable("Interaction token is null"))
return
}
val loanConfigObj = ScLoanInfo(interactionToken)
val loanConfigObj = ScLoanInfo(interactionToken, parseColorScheme(hashMap["colorScheme"]))
ScLoan.withdraw(appCompatActivity, loanConfigObj, object : ScLoanResult {
override fun onFailure(error: ScLoanError) {
promise.reject("${error.code}", scLoanResponseToWritableMap(error) ?: return)
Expand All @@ -424,7 +424,7 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
promise.reject(Throwable("Interaction token is null"))
return
}
val loanConfigObj = ScLoanInfo(interactionToken)
val loanConfigObj = ScLoanInfo(interactionToken, parseColorScheme(hashMap["colorScheme"]))
ScLoan.service(appCompatActivity, loanConfigObj, object : ScLoanResult {
override fun onFailure(error: ScLoanError) {
promise.reject("${error.code}", scLoanResponseToWritableMap(error) ?: return)
Expand All @@ -448,7 +448,7 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
promise.reject(Throwable("Interaction token is null"))
return
}
val loanConfigObj = ScLoanInfo(interactionToken)
val loanConfigObj = ScLoanInfo(interactionToken, parseColorScheme(hashMap["colorScheme"]))
ScLoan.triggerInteraction(appCompatActivity, loanConfigObj, object : ScLoanResult {
override fun onFailure(error: ScLoanError) {
promise.reject("${error.code}", scLoanResponseToWritableMap(error) ?: return)
Expand All @@ -460,6 +460,16 @@ class SmallcaseGatewayModule(reactContext: ReactApplicationContext) : ReactConte
})
}

// colorScheme rides the bridge as a primitive string (not a serialized object) and is
// mapped back to the native enum here. Unknown/absent → null, so the SDK keeps its
// "partner sent nothing → light, don't force theme param" default.
private fun parseColorScheme(value: String?): ScLoanColorScheme? = when (value) {
"dark" -> ScLoanColorScheme.DARK
"light" -> ScLoanColorScheme.LIGHT
"system" -> ScLoanColorScheme.SYSTEM
else -> null
}

private fun getProtocol(envName: String): Environment.PROTOCOL {
return when (envName) {
"production" -> Environment.PROTOCOL.PRODUCTION
Expand Down
28 changes: 23 additions & 5 deletions ios/SmallcaseGateway.m
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,24 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
});
}

// colorScheme rides the bridge as a primitive string (not a serialized object) and is
// mapped back to the native enum here. Unknown/absent → the token-only initializer, so the
// SDK keeps its "partner sent nothing → light, don't force theme param" default.
- (ScLoanInfo *)loanInfoFromDict:(NSDictionary *)loanInfo {
NSString *interactionToken = loanInfo[@"interactionToken"];
id scheme = loanInfo[@"colorScheme"];
if ([scheme isKindOfClass:[NSString class]]) {
if ([scheme isEqualToString:@"dark"]) {
return [[ScLoanInfo alloc] initWithInteractionToken:interactionToken colorScheme:ScLoanColorSchemeDark];
} else if ([scheme isEqualToString:@"light"]) {
return [[ScLoanInfo alloc] initWithInteractionToken:interactionToken colorScheme:ScLoanColorSchemeLight];
} else if ([scheme isEqualToString:@"system"]) {
return [[ScLoanInfo alloc] initWithInteractionToken:interactionToken colorScheme:ScLoanColorSchemeSystem];
}
}
return [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
}

RCT_REMAP_METHOD(apply,
loanInfo: (NSDictionary *)loanInfo
applyWithResolver:(RCTPromiseResolveBlock)resolve
Expand All @@ -640,7 +658,7 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
NSString *interactionToken = loanInfo[@"interactionToken"];
NSLog(@" ----------- Interaction Token: %@", interactionToken);

ScLoanInfo *gatewayLoanInfo = [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
ScLoanInfo *gatewayLoanInfo = [self loanInfoFromDict:loanInfo];

[ScLoan.instance applyWithPresentingController:[[[UIApplication sharedApplication] keyWindow] rootViewController] loanInfo:gatewayLoanInfo completion:^(ScLoanSuccess * success, ScLoanError * error) {

Expand All @@ -667,7 +685,7 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
NSString *interactionToken = loanInfo[@"interactionToken"];
NSLog(@" ----------- Interaction Token: %@", interactionToken);

ScLoanInfo *gatewayLoanInfo = [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
ScLoanInfo *gatewayLoanInfo = [self loanInfoFromDict:loanInfo];

[ScLoan.instance payWithPresentingController:[[[UIApplication sharedApplication] keyWindow] rootViewController] loanInfo:gatewayLoanInfo completion:^(ScLoanSuccess * success, ScLoanError * error) {

Expand All @@ -694,7 +712,7 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
NSString *interactionToken = loanInfo[@"interactionToken"];
NSLog(@" ----------- Interaction Token: %@", interactionToken);

ScLoanInfo *gatewayLoanInfo = [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
ScLoanInfo *gatewayLoanInfo = [self loanInfoFromDict:loanInfo];

[ScLoan.instance withdrawWithPresentingController:[[[UIApplication sharedApplication] keyWindow] rootViewController] loanInfo:gatewayLoanInfo completion:^(ScLoanSuccess * success, ScLoanError * error) {

Expand All @@ -721,7 +739,7 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
NSString *interactionToken = loanInfo[@"interactionToken"];
NSLog(@" ----------- Interaction Token: %@", interactionToken);

ScLoanInfo *gatewayLoanInfo = [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
ScLoanInfo *gatewayLoanInfo = [self loanInfoFromDict:loanInfo];

[ScLoan.instance serviceWithPresentingController:[[[UIApplication sharedApplication] keyWindow] rootViewController] loanInfo:gatewayLoanInfo completion:^(ScLoanSuccess * success, ScLoanError * error) {

Expand Down Expand Up @@ -749,7 +767,7 @@ @interface RCT_EXTERN_MODULE(SmallcaseGateway, NSObject)
NSString *interactionToken = loanInfo[@"interactionToken"];
NSLog(@" ----------- Interaction Token: %@", interactionToken);

ScLoanInfo *gatewayLoanInfo = [[ScLoanInfo alloc] initWithInteractionToken:interactionToken];
ScLoanInfo *gatewayLoanInfo = [self loanInfoFromDict:loanInfo];

[ScLoan.instance triggerInteractionWithPresentingController:[[[UIApplication sharedApplication] keyWindow] rootViewController] loanInfo:gatewayLoanInfo completion:^(ScLoanSuccess * success, ScLoanError * error) {

Expand Down
4 changes: 3 additions & 1 deletion react-native-smallcase-gateway.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@ Pod::Spec.new do |s|
end

s.dependency 'SCGateway', '7.2.0'
s.dependency 'SCLoans', '7.1.2'
# INTERNAL TEST PIN: dark-theme branch build (mirrors the android/build.gradle pin).
# Revert to: s.dependency 'SCLoans', '7.2.0'
s.dependency 'SCLoans-sourav-native-dark-theme-0e85bd6', '7.1.2-44-release'
end
2 changes: 1 addition & 1 deletion smart_investing_react_native/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ react {
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
def enableProguardInReleaseBuilds = true

/**
* The preferred build flavor of JavaScriptCore (JSC)
Expand Down
32 changes: 32 additions & 0 deletions smart_investing_react_native/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,35 @@
# http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# SCLoans ships -repackageclasses 'com.smallcase.loans' as a consumer rule, which
# causes the host R8 to move 6 000+ external classes into com.smallcase.loans —
# colliding with the SDK's own pre-obfuscated classes and corrupting the Koin DI
# type graph at runtime (ClassCastException in wi/cm/o40 chain).
# -keeppackagenames overrides -repackageclasses per the ProGuard/R8 spec.
-keeppackagenames **

# SCGateway (com.smallcase.gateway:sdk) ships NO consumer proguard rules and its
# classes enter R8 with clear names, so the host R8 (full mode) freely obfuscates
# and merges its Dagger factories and Retrofit interfaces. That breaks
# retrofit.create(GatewayApiService) — the proxy cast fails at runtime with a
# ClassCastException routed through R8's synthetic ThrowCCE helper
# (FakeNetworkModule.provideGatewayApiService chain). Keep the SDK intact,
# mirroring the -keep that SCLoans already ships for itself.
-keep class com.smallcase.gateway.** { *; }

# Retrofit + R8 full mode (AGP 8 default): generic signatures are stripped for
# any type that is not kept, so ConfigService.getBrokerConfigs(): Call<Foo>
# degrades to a raw Call and Retrofit rejects it
# ("Call return type must be parameterized as Call<Foo>"). These are the
# canonical rules Retrofit 2.9+ bundles in META-INF/proguard/retrofit2.pro; the
# gateway SDK vendors an older Retrofit whose embedded rules never reach the host,
# so we declare them here. allowobfuscation/allowshrinking keep the types
# "kept enough" to retain their generic signatures while still letting R8 rename them.
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
6 changes: 5 additions & 1 deletion smart_investing_react_native/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ hermesEnabled=true

artifactory_contextUrl=https\://artifactory.smallcase.com/artifactory
artifactory_password=reactNativeUser123
artifactory_user=react_native_user
artifactory_user=react_native_user

# R8 full mode stress test — loans SDK consumer-rules.pro verified safe 2026-07-01
# Remove after confirming release build passes all P0 QA cases
android.enableR8.fullMode=true
57 changes: 42 additions & 15 deletions smart_investing_react_native/app/screens/LoansScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,23 @@ import {
import {SIJsonViewer} from '../components/SIJsonViewer';
import {SmartButton} from './HoldingsScreen';
import {SIDropDown} from '../components/SIDropdown';
import SegmentedControl from '@react-native-segmented-control/segmented-control';
import {ScLoan} from 'react-native-smallcase-gateway';

type LoanSummaryType = Awaited<ReturnType<typeof getUnityUserLoanSummary>>;

// Color scheme passed to the LAS web UI. Index 0 ("None") omits colorScheme
// entirely so the SDK keeps its "partner sent nothing → light" default; the
// rest map to the primitive string the native bridge expects.
type ScLoanColorScheme = 'light' | 'dark' | 'system';
const COLOR_SCHEME_LABELS = ['None', 'Light', 'Dark', 'System'];
const COLOR_SCHEME_VALUES: (ScLoanColorScheme | undefined)[] = [
undefined,
'light',
'dark',
'system',
];

export const basePayload = {
intent: 'LOAN_APPLICATION',
config: {
Expand Down Expand Up @@ -63,6 +76,16 @@ const LoansScreen = ({route}: {route: any}) => {
const [loanSummary, setLoanSummary] = useState<LoanSummaryType>();
const [offersInput] = useState<string>('');

// Selected LAS color scheme; defaults to index 0 ("None").
const [colorSchemeIndex, setColorSchemeIndex] = useState<number>(0);

// Single source of truth for the ScLoanInfo passed to every trigger. When the
// selected scheme is "None" we omit the key so the SDK applies its default.
const buildLoanInfo = (token: string) => {
const colorScheme = COLOR_SCHEME_VALUES[colorSchemeIndex];
return colorScheme ? {interactionToken: token, colorScheme} : {interactionToken: token};
};

const [createUnityUserPayload, setCreateUnityUserPayload] =
useState<GenerateUnityUserPayload>({
id: '',
Expand Down Expand Up @@ -227,9 +250,7 @@ const LoansScreen = ({route}: {route: any}) => {
if (typeof interactionToken !== 'string') {
throw new Error('Invalid interaction token!');
}
const applyRes = await ScLoan.apply({
interactionToken: interactionToken,
});
const applyRes = await ScLoan.apply(buildLoanInfo(interactionToken));
alert('Success', `${JSON.stringify(applyRes)}`);
} catch (error: any) {
alert('Error', `${error}, ${JSON.stringify(error.userInfo)}`);
Expand All @@ -241,9 +262,7 @@ const LoansScreen = ({route}: {route: any}) => {
if (typeof interactionToken !== 'string') {
throw new Error('Invalid interaction token!');
}
const payRes = await ScLoan.pay({
interactionToken: interactionToken,
});
const payRes = await ScLoan.pay(buildLoanInfo(interactionToken));
alert('Success', `${JSON.stringify(payRes)}`);
} catch (error: any) {
alert('Error', `${error}, ${JSON.stringify(error.userInfo)}`);
Expand All @@ -255,9 +274,7 @@ const LoansScreen = ({route}: {route: any}) => {
if (typeof interactionToken !== 'string') {
throw new Error('Invalid interaction token!');
}
const withdrawRes = await ScLoan.withdraw({
interactionToken: interactionToken,
});
const withdrawRes = await ScLoan.withdraw(buildLoanInfo(interactionToken));
alert('Success', `${JSON.stringify(withdrawRes)}`);
} catch (error: any) {
alert('Error', `${error}, ${JSON.stringify(error.userInfo)}`);
Expand All @@ -269,9 +286,7 @@ const LoansScreen = ({route}: {route: any}) => {
if (typeof interactionToken !== 'string') {
throw new Error('Invalid interaction token!');
}
const serviceRes = await ScLoan.service({
interactionToken: interactionToken,
});
const serviceRes = await ScLoan.service(buildLoanInfo(interactionToken));
alert('Success', `${JSON.stringify(serviceRes)}`);
} catch (error: any) {
alert('Error', `${error}, ${JSON.stringify(error.userInfo)}`);
Expand All @@ -283,9 +298,9 @@ const LoansScreen = ({route}: {route: any}) => {
if (typeof interactionToken !== 'string') {
throw new Error('Invalid interaction token!');
}
const serviceRes = await ScLoan.triggerInteraction({
interactionToken: interactionToken,
});
const serviceRes = await ScLoan.triggerInteraction(
buildLoanInfo(interactionToken),
);
alert('Success', `${JSON.stringify(serviceRes)}`);
} catch (error: any) {
alert('Error', `${error}, ${JSON.stringify(error.userInfo)}`);
Expand Down Expand Up @@ -515,6 +530,18 @@ const LoansScreen = ({route}: {route: any}) => {
}}
title={'Setup'}
/>
<View style={{height: 12, backgroundColor: 'transparent'}} />
<Text style={{fontSize: 14, fontWeight: 'bold', marginBottom: 6}}>
Color Scheme
</Text>
<SegmentedControl
values={COLOR_SCHEME_LABELS}
selectedIndex={colorSchemeIndex}
onChange={event => {
setColorSchemeIndex(event.nativeEvent.selectedSegmentIndex);
}}
/>
<View style={{height: 12, backgroundColor: 'transparent'}} />
<ScButton onPress={applyForLoan} title={'Apply'} />
<ScButton onPress={payAmount} title={'Pay'} />
<ScButton onPress={withdrawAmount} title={'Withdraw'} />
Expand Down
19 changes: 10 additions & 9 deletions smart_investing_react_native/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1410,14 +1410,14 @@ PODS:
- Yoga
- react-native-segmented-control (2.5.7):
- React-Core
- react-native-smallcase-gateway (7.3.6):
- react-native-smallcase-gateway (7.4.3):
- RCTRequired
- RCTTypeSafety
- React-Codegen
- React-Core
- ReactCommon/turbomodule/core
- SCGateway (= 7.1.7)
- SCLoans (= 7.1.2)
- SCGateway (= 7.2.0)
- SCLoans-sourav-native-dark-theme-0e85bd6 (= 7.1.2-44-release)
- React-NativeModulesApple (0.79.4):
- glog
- hermes-engine
Expand Down Expand Up @@ -1839,8 +1839,8 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SCGateway (7.1.7)
- SCLoans (7.1.2)
- SCGateway (7.2.0)
- SCLoans-sourav-native-dark-theme-0e85bd6 (7.1.2-44-release)
- SocketRocket (0.7.1)
- Yoga (0.0.0)

Expand Down Expand Up @@ -1925,10 +1925,11 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

SPEC REPOS:
https://github.com/smallcase/cocoapodspec-internal.git:
- SCLoans-sourav-native-dark-theme-0e85bd6
trunk:
- React-Codegen
- SCGateway
- SCLoans
- SocketRocket

EXTERNAL SOURCES:
Expand Down Expand Up @@ -2128,7 +2129,7 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: ef2292ca147fa8793305e4693586ad0caf3afad3
react-native-safe-area-context: 562163222d999b79a51577eda2ea8ad2c32b4d06
react-native-segmented-control: bf6e0032726727498e18dd437ae88afcdbc18e99
react-native-smallcase-gateway: 5af44ebaee1d5326befe862f0cf8cb00143f1098
react-native-smallcase-gateway: 8e5836a403989efd924992c552fbfdf57dea8a65
React-NativeModulesApple: da60186ad0aafff031a9bc86b048711d34acc813
React-oscompat: 472a446c740e39ee39cd57cd7bfd32177c763a2b
React-perflogger: bbca3688c62f4f39e972d6e21969c95fe441fb6c
Expand Down Expand Up @@ -2163,8 +2164,8 @@ SPEC CHECKSUMS:
RNCClipboard: 37de6995ef72dc869422879e51a46a520d3f08b3
RNGestureHandler: ebef699ea17e7c0006c1074e1e423ead60ce0121
RNScreens: 3f6e41f9f89cb888a6b4b27566bdfdf7a3bf51ad
SCGateway: a0f1bc86e449c7d440d8661e0936095b6f345179
SCLoans: 4accc0a0a483f3943cc513a50800485201fed4dd
SCGateway: 8089d79c55ecc8e7b581e0bf56fd28a2e3d957d0
SCLoans-sourav-native-dark-theme-0e85bd6: 98e7ffbfcc0da3bc0571caeaaf353ae4a7914dfa
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: a6cb833e04fb8c59a012b49fb1d040fcb0cbb633

Expand Down
Loading
Loading