diff --git a/README.md b/README.md index faa5053..a1cc9f8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The epotheke SDK provides an API which can be integrated directly using the `Sdk This class needs to be instantiated and given the following: - `cardLinkUrl` the url of the CardLink service to use +- `tenantToken` a JWT provided by the service provider, for environments allowing non-authenticated acces it can be null - implementation of `CardLinkControllerCallback` for providing the CardLink result and protocols for subsequent processes (e.i. Prescription retrieval/selection) - implementation of `CardLinkInteraction` for exchanging data between the user and the CardLink service - implementation of `SdkErrorHandler` for handling errors related to the SDK initialisation diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 67a5bfa..e813256 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,7 +9,6 @@ dependencies { implementation(libs.plugins.kotlinCocoapods) implementation(libs.plugins.androidLibrary) implementation(libs.plugins.androidApplication) - implementation(libs.plugins.jetbrainsCompose) implementation(libs.plugins.kotlinJvm) implementation(libs.plugins.kotlinAllOpen) diff --git a/buildSrc/src/main/kotlin/epotheke.app-android-conventions.gradle.kts b/buildSrc/src/main/kotlin/epotheke.app-android-conventions.gradle.kts index d5b5359..c762729 100644 --- a/buildSrc/src/main/kotlin/epotheke.app-android-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/epotheke.app-android-conventions.gradle.kts @@ -4,7 +4,6 @@ val androidMinSdk: String by project plugins { id("epotheke.kotlin-conventions") id("com.android.application") - id("org.jetbrains.compose") } kotlin { diff --git a/buildSrc/src/main/kotlin/epotheke.lib-ios-conventions.gradle.kts b/buildSrc/src/main/kotlin/epotheke.lib-ios-conventions.gradle.kts index c533052..cd55476 100644 --- a/buildSrc/src/main/kotlin/epotheke.lib-ios-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/epotheke.lib-ios-conventions.gradle.kts @@ -1,5 +1,3 @@ -import gradle.kotlin.dsl.accessors._e17afd335abbdd9764f13315a15fdf50.kotlin - plugins { id("epotheke.kotlin-conventions") id("org.jetbrains.kotlin.native.cocoapods") diff --git a/cardlink-mock/src/main/kotlin/com/epotheke/cardlink/mock/WSModel.kt b/cardlink-mock/src/main/kotlin/com/epotheke/cardlink/mock/WSModel.kt index 8403363..9ff08c1 100644 --- a/cardlink-mock/src/main/kotlin/com/epotheke/cardlink/mock/WSModel.kt +++ b/cardlink-mock/src/main/kotlin/com/epotheke/cardlink/mock/WSModel.kt @@ -73,7 +73,7 @@ object GematikMessageSerializer : KSerializer { } val base64EncodedPayload: String = value.payload.let { val payloadJsonStr = cardLinkJsonFormatter.encodeToString(it) - Base64.encode(payloadJsonStr.encodeToByteArray()).trimEnd('=') + Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL).encode(payloadJsonStr.encodeToByteArray()) } val jsonPayload = buildJsonObject { put("type", payloadType) @@ -108,7 +108,9 @@ object GematikMessageSerializer : KSerializer { @OptIn(ExperimentalEncodingApi::class) fun toTypedJsonElement(base64EncodedPayload: String, payloadType: String) : JsonObject { - val jsonPayload = String(Base64.decode(base64EncodedPayload)) + val jsonPayload = Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL) + .decode(base64EncodedPayload) + .toString(Charsets.UTF_8) val jsonElement = Json.parseToJsonElement(jsonPayload) return JsonObject(jsonElement.jsonObject.toMutableMap().apply { put(Json.configuration.classDiscriminator, JsonPrimitive(payloadType)) @@ -124,12 +126,14 @@ object ByteArrayAsBase64Serializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ByteArrayAsBase64Serializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - val base64Encoded = Base64.encode(value).trimEnd('=') - encoder.encodeString(base64Encoded) + encoder.encodeString( + Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL).encode(value) + ) } override fun deserialize(decoder: Decoder): ByteArray { - return Base64.decode(decoder.decodeString()) + return Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL) + .decode(decoder.decodeString()) } } diff --git a/demo-android-standalone/app/build.gradle.kts b/demo-android-standalone/app/build.gradle.kts index cc9a33d..7545531 100644 --- a/demo-android-standalone/app/build.gradle.kts +++ b/demo-android-standalone/app/build.gradle.kts @@ -44,7 +44,7 @@ android { } } -val epothekeSdkVersion = "1.1.10" +val epothekeSdkVersion = "1.1.11-SNAPSHOT" dependencies { androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/demo-android-standalone/app/src/main/assets/logback.xml b/demo-android-standalone/app/src/main/assets/logback.xml index 377c547..7083e36 100644 --- a/demo-android-standalone/app/src/main/assets/logback.xml +++ b/demo-android-standalone/app/src/main/assets/logback.xml @@ -17,7 +17,7 @@ - + diff --git a/demo-android-standalone/app/src/main/java/com/epotheke/demo/SdkActivityImp.kt b/demo-android-standalone/app/src/main/java/com/epotheke/demo/SdkActivityImp.kt index 717b848..d9d0a55 100644 --- a/demo-android-standalone/app/src/main/java/com/epotheke/demo/SdkActivityImp.kt +++ b/demo-android-standalone/app/src/main/java/com/epotheke/demo/SdkActivityImp.kt @@ -134,6 +134,11 @@ class SdkActivityImp : SdkActivity() { } } + override fun getTenantToken(): String? { + //if environment allows unauthenticated access tenantToken can be null + return null + } + /** * Implementation of the CardLinkInteraction * The different methods get called during the interaction with the card and may require @@ -144,7 +149,7 @@ class SdkActivityImp : SdkActivity() { * Called when the SDK needs to communicates with the card. * The user must read the CAN from it card and enter it within the app. * The given CAN has then be handed back to the SDK via - * confirmPasswordOperation.confirmPassowrd("") + * confirmPasswordOperation.confirmPassword("") * * @param confirmPasswordOperation */ @@ -154,6 +159,16 @@ class SdkActivityImp : SdkActivity() { confirmPasswordOperation.confirmPassword(value) } } + /** + * Called if something went wrong in the last step. + * Information can be gathered by errCode and errMsg. + */ + override fun onCanRetry(confirmPasswordOperation: ConfirmPasswordOperation, errCode: CardLinkErrorCodes.ClientCodes?, errMessage: String?) { + LOG.debug { "epotheke implementation onCanRetry, code: $errCode - msg: $errMessage" } + getValueFromUser("Problem with CAN. Please try again and provide CAN", "000000") { value -> + confirmPasswordOperation.confirmPassword(value) + } + } /** @@ -176,6 +191,20 @@ class SdkActivityImp : SdkActivity() { } } + /** + * Called if something went wrong in the last step. + * Information can be gathered by errCode and errMsg. + */ + override fun onPhoneNumberRetry(confirmTextOperation: ConfirmTextOperation, errCode: CardLinkErrorCodes.CardLinkCodes?, errMsg: String?) { + LOG.debug { "epotheke implementation onPhoneRetry, code: $errCode - msg: $errMsg" } + getValueFromUser( + "Problem with phone number. Please try again", + "+4915123456789" + ) { value -> + confirmTextOperation.confirmText(value) + } + } + /** * Called during the cardlink establishment. * The user will get an SMS containing a TAN for verification purposes. @@ -194,6 +223,20 @@ class SdkActivityImp : SdkActivity() { confirmPasswordOperation.confirmPassword(value) } } + /** + * Called if something went wrong in the last step. + * Information can be gathered by errCode and errMsg. + */ + override fun onSmsCodeRetry(confirmPasswordOperation: ConfirmPasswordOperation, errCode: CardLinkErrorCodes.CardLinkCodes?, errMsg: String?) { + LOG.debug { "epotheke implementation onSmsCodeRetry, code: $errCode - msg: $errMsg" } + getValueFromUser( + "Problem with TAN. Please try again", + "123456" + ) { value -> + confirmPasswordOperation?.confirmPassword(value) + } + + } /** * Called during the connection establishment, when the SDK has to communicate with the card. diff --git a/demo-ios-standalone/EpothekeDemo/epothekeDemo/ContentView.swift b/demo-ios-standalone/EpothekeDemo/epothekeDemo/ContentView.swift index a628256..7d22901 100644 --- a/demo-ios-standalone/EpothekeDemo/epothekeDemo/ContentView.swift +++ b/demo-ios-standalone/EpothekeDemo/epothekeDemo/ContentView.swift @@ -223,7 +223,10 @@ struct ContentView: View { let sdkErrorHandler = SdkErrorHandlerImp() let cardLinkInteraction = CardLinkInteraction(v: self) let url = "https://mock.test.epotheke.com/cardlink?token="+RandomUUID_iosKt.randomUUID() + //if environment allows unauthenticated access tenantToken can be null + let tenantToken : String? = nil let sdk = SdkCore(cardLinkUrl: url, + tenantToken: tenantToken, cardLinkControllerCallback: cardLinkController, cardLinkInteractionProtocol: cardLinkInteraction, sdkErrorHandler: sdkErrorHandler, diff --git a/demo-react-native/App.tsx b/demo-react-native/App.tsx index 1e89550..bb892d4 100644 --- a/demo-react-native/App.tsx +++ b/demo-react-native/App.tsx @@ -87,6 +87,15 @@ function App(): React.JSX.Element { }; SdkModule.set_cardlinkInteractionCB_onCanRequest(canRequestCB); + let canRetryCB = (code: String | undefined, msg: String | undefined) => { + log('canRetryCB'); + SdkModule.set_cardlinkInteractionCB_onCanRetry(canRetryCB); + setInputValue('123123'); + setmodalTxt('Retry Can due to: ' + code + ' - ' + msg); + toggleModalVisibility(); + }; + SdkModule.set_cardlinkInteractionCB_onCanRetry(canRetryCB); + let onPhoneNumberRequestCB = () => { log('onPhoneNumberRequest'); SdkModule.set_cardlinkInteractionCB_onPhoneNumberRequest(onPhoneNumberRequestCB); @@ -96,6 +105,15 @@ function App(): React.JSX.Element { }; SdkModule.set_cardlinkInteractionCB_onPhoneNumberRequest(onPhoneNumberRequestCB); + let onPhoneNumberRetryCB = (code: String | undefined, msg: String | undefined) => { + log('onPhoneNumberRetryCB'); + SdkModule.set_cardlinkInteractionCB_onPhoneNumberRetry(onPhoneNumberRetryCB); + setmodalTxt('Retry Number due to: ' + code + ' - ' + msg); + setInputValue('015111122233'); + toggleModalVisibility(); + }; + SdkModule.set_cardlinkInteractionCB_onPhoneNumberRetry(onPhoneNumberRetryCB); + let onSmsCodeRequestCB = () => { log('onSmsCodeRequest'); SdkModule.set_cardlinkInteractionCB_onSmsCodeRequest(onSmsCodeRequestCB); @@ -105,6 +123,16 @@ function App(): React.JSX.Element { }; SdkModule.set_cardlinkInteractionCB_onSmsCodeRequest(onSmsCodeRequestCB); + let onSmsCodeRetryCB = (code: String | undefined, msg: String | undefined) => { + log('onSmsCodeRetryCB'); + SdkModule.set_cardlinkInteractionCB_onSmsCodeRetry(onSmsCodeRetryCB); + setmodalTxt('Retry TAN due to: ' + code + ' - ' + msg); + setInputValue('123456789'); + toggleModalVisibility(); + }; + SdkModule.set_cardlinkInteractionCB_onSmsCodeRetry(onSmsCodeRetryCB); + + /* Called if the sdk runs into an error. */ @@ -134,31 +162,31 @@ function App(): React.JSX.Element { */ let onAuthenticationCallback = async (err: any, msg: any) => { if(err){ - log(`onAuthenticationCompletion error: ${err}`); + log(`onAuthenticationCompletion error: ${err} - ${msg}`); } else { - log(`onAuthenticationCompletion protos: ${msg}`); - try { + log(`success`); + //get available prescriptions let availPrescriptions = await SdkModule.getPrescriptions(); log(`prescriptions: ${availPrescriptions}`); - //example for a selection - //which has to be done via a jsonstring containing the selectedPrescriptionList - let confirmation = await SdkModule.selectPrescriptions(`{ - "type": "selectedPrescriptionList", - "ICCSN": "MTIzNDU2Nzg5", - "prescriptionIndexList": [ - "160.000.764.737.300.50", - "160.100.000.000.012.06", - "160.100.000.000.004.30", - "160.100.000.000.014.97", - "160.100.000.000.006.24" - ], - "supplyOptionsType": "delivery", - "messageId": "bad828ad-75fa-4eea-aea5-a3587d95ce4a" - }`); - log(`selection confirmation: ${confirmation}`); + ////example for a selection + ////which has to be done via a jsonstring containing the selectedPrescriptionList + //let confirmation = await SdkModule.selectPrescriptions(`{ + // "type": "selectedPrescriptionList", + // "ICCSN": "MTIzNDU2Nzg5", + // "prescriptionIndexList": [ + // "160.000.764.737.300.50", + // "160.100.000.000.012.06", + // "160.100.000.000.004.30", + // "160.100.000.000.014.97", + // "160.100.000.000.006.24" + // ], + // "supplyOptionsType": "delivery", + // "messageId": "bad828ad-75fa-4eea-aea5-a3587d95ce4a" + // }`); + //log(`selection confirmation: ${confirmation}`); } catch (e) { log(`error : ${e}`); } @@ -169,7 +197,9 @@ function App(): React.JSX.Element { SdkModule.set_controllerCallbackCB_onAuthenticationCompletion(onAuthenticationCallback); // start the CardLink establishment - SdkModule.startCardLink(`https://mock.test.epotheke.com/cardlink?token=${uuid.v4()}`); + //SdkModule.startCardLink(`https://service.dev.epotheke.com/cardlink?token=${uuid.v4()}`, `TENANTTOKEN`); + //When the environment allows unauthenticated connection, TENANTTOKEN can be null + SdkModule.startCardLink(`https://mock.test.epotheke.com/cardlink?token=${uuid.v4()}`, null); } const log = (msg: string) => { diff --git a/demo-react-native/android/app/build.gradle b/demo-react-native/android/app/build.gradle index 003b4dd..1c2abd2 100644 --- a/demo-react-native/android/app/build.gradle +++ b/demo-react-native/android/app/build.gradle @@ -123,7 +123,7 @@ android { } } -def epothekeSdkVersion = "1.1.10" +def epothekeSdkVersion = "1.1.11-SNAPSHOT" dependencies { // The version of react-native is set by the React Native Gradle Plugin diff --git a/demo-react-native/android/app/src/main/java/com/demoreactnative/SdkModule.kt b/demo-react-native/android/app/src/main/java/com/demoreactnative/SdkModule.kt index 8078856..cd49701 100644 --- a/demo-react-native/android/app/src/main/java/com/demoreactnative/SdkModule.kt +++ b/demo-react-native/android/app/src/main/java/com/demoreactnative/SdkModule.kt @@ -6,10 +6,10 @@ import com.epotheke.erezept.model.AvailablePrescriptionLists import com.epotheke.erezept.model.RequestPrescriptionList import com.epotheke.erezept.model.SelectedPrescriptionList import com.epotheke.erezept.model.prescriptionJsonFormatter -import com.epotheke.sdk.CardLinkProtocol import com.epotheke.sdk.CardLinkControllerCallback -import com.epotheke.sdk.SdkCore +import com.epotheke.sdk.CardLinkProtocol import com.epotheke.sdk.PrescriptionProtocol +import com.epotheke.sdk.SdkCore import com.epotheke.sdk.SdkErrorHandler import com.facebook.react.bridge.ActivityEventListener import com.facebook.react.bridge.Callback @@ -22,6 +22,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import org.openecard.mobile.activation.ActivationResult +import org.openecard.mobile.activation.CardLinkErrorCodes import org.openecard.mobile.activation.CardLinkInteraction import org.openecard.mobile.activation.ConfirmPasswordOperation import org.openecard.mobile.activation.ConfirmTextOperation @@ -98,13 +99,20 @@ class SdkModule(private val reactContext: ReactApplicationContext) : cardLinkProtocols: Set ) { logger.debug { "onAuthenticationCompletion ${p0?.errorMessage}" } - erezeptProtocol = cardLinkProtocols.filterIsInstance().first() - val availableProtocols = - cardLinkProtocols.joinToString(prefix = "protocols: ") { p -> p.javaClass.name } - onAuthenticationCompletionCB?.invoke(p0?.errorMessage, availableProtocols) - + //hotfix + if(p0?.errorMessage?.contains("==>") == true){ + var minor = p0?.errorMessage?.split("==>")?.get(0)?.trim() + var msg = p0?.errorMessage?.split("==>")?.get(1)?.trim() + onAuthenticationCompletionCB?.invoke(minor, msg) + } else if (p0?.errorMessage != null){ + onAuthenticationCompletionCB?.invoke(p0?.resultCode?.name, p0?.errorMessage) + } else { + onAuthenticationCompletionCB?.invoke(null, null) + } +// onAuthenticationCompletionCB?.invoke(p0?.processResultMinor, p0?.errorMessage) +// onAuthenticationCompletionCB?.invoke(minor, msg) } override fun onStarted() { @@ -148,6 +156,13 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onCanRequestCB = cb } + var onCanRetryCB: Callback? = null + + @ReactMethod + fun set_cardlinkInteractionCB_onCanRetry(cb: Callback) { + onCanRetryCB = cb + } + var onPhoneNumberRequestCB: Callback? = null @ReactMethod @@ -155,6 +170,13 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onPhoneNumberRequestCB = cb } + var onPhoneNumberRetryCB: Callback? = null + + @ReactMethod + fun set_cardlinkInteractionCB_onPhoneNumberRetry(cb: Callback) { + onPhoneNumberRetryCB = cb + } + var onSmsCodeRequestCB: Callback? = null @ReactMethod @@ -162,6 +184,13 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onSmsCodeRequestCB = cb } + var onSmsCodeRetryCB: Callback? = null + + @ReactMethod + fun set_cardlinkInteractionCB_onSmsCodeRetry(cb: Callback) { + onSmsCodeRetryCB = cb + } + private val cardLinkInteraction = object : CardLinkInteraction { override fun requestCardInsertion() { logger.debug { "requestCardInsertion" } @@ -200,6 +229,17 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onCanRequestCB?.invoke() } + override fun onCanRetry(p0: ConfirmPasswordOperation?, p1: String?, p2: String?) { + logger.debug { "onCanRetry" } + p0?.let { + userInputDispatch = { s -> + logger.debug { "confirming number $s with framework interaction" } + p0.confirmPassword(s) + } + } + onCanRetryCB?.invoke(p1, p2) + } + override fun onPhoneNumberRequest(p0: ConfirmTextOperation?) { logger.debug { "onPhoneNumberRequest" } p0?.let { @@ -211,6 +251,17 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onPhoneNumberRequestCB?.invoke() } + override fun onPhoneNumberRetry(p0: ConfirmTextOperation?, p1: String?, p2: String?) { + logger.debug { "onPhoneNumberRetry" } + p0?.let { + userInputDispatch = { s -> + logger.debug { "confirming number $s with framework interaction" } + p0.confirmText(s) + } + } + onPhoneNumberRetryCB?.invoke(p1,p2) + } + override fun onSmsCodeRequest(p0: ConfirmPasswordOperation?) { logger.debug { "onSmsCodeRequest" } p0?.let { @@ -222,6 +273,17 @@ class SdkModule(private val reactContext: ReactApplicationContext) : onSmsCodeRequestCB?.invoke() } + override fun onSmsCodeRetry(p0: ConfirmPasswordOperation?, p1: String?, p2: String?) { + logger.debug { "onSmsCodeRetry" } + p0?.let { + userInputDispatch = { s -> + logger.debug { "confirming tan $s with framework interaction" } + p0.confirmPassword(s) + } + } + onSmsCodeRetryCB?.invoke(p1,p2) + } + } var onErrorCB: Callback? = null @@ -233,7 +295,7 @@ class SdkModule(private val reactContext: ReactApplicationContext) : private val errorHandler = object : SdkErrorHandler { override fun onError(error: ServiceErrorResponse) { logger.debug { "SdkModule onError will call registered RN callback with: ${error.errorMessage}" } - onErrorCB?.invoke(null, error.errorMessage) + onErrorCB?.invoke(error.statusCode.name, error.errorMessage) } } @@ -247,8 +309,9 @@ class SdkModule(private val reactContext: ReactApplicationContext) : var epothekeInstance : SdkCore? = null @ReactMethod - fun startCardLink(cardLinkUrl: String) { + fun startCardLink(cardLinkUrl: String, tenantToken: String?) { logger.debug { "SdkModule called with url : $cardLinkUrl" } + logger.debug { "SdkModule called with tenantToken: $tenantToken" } epothekeInstance?.let { it.destroyOecContext() @@ -258,6 +321,7 @@ class SdkModule(private val reactContext: ReactApplicationContext) : val epotheke = SdkCore( activity, cardLinkUrl, + tenantToken, cardLinkControllerCallback, cardLinkInteraction, errorHandler, diff --git a/demo-react-native/ios/RCTSdkModule.m b/demo-react-native/ios/RCTSdkModule.m index 82ad406..022d4ed 100644 --- a/demo-react-native/ios/RCTSdkModule.m +++ b/demo-react-native/ios/RCTSdkModule.m @@ -52,7 +52,7 @@ - (void)hdlError:(NSObject *_Nullable)error { if ([error conformsToProtocol:@protocol(ServiceErrorResponse)]) { RCTLogInfo(@"error msg: %@", [error getErrorMessage]); - self.onSdkErrorCB(@[ @"Error", [error getErrorMessage] ]); + self.onSdkErrorCB(@[ @"INTERNAL_ERROR", [error getErrorMessage] ]); } } @end @@ -67,19 +67,25 @@ @implementation CardLinkControllerCallback - (void)onAuthenticationCompletionP0:(id _Nullable)p0 cardLinkProtocols:(nonnull NSSet> *)cardLinkProtocols { RCTLogInfo(@"onAuthComp"); - if([p0 getResultCode] == 0){ + if([p0 getResultCode] == kActivationResultCodeOK){ if (self.onAuthenticationCompletionCB) { for (NSObject *p in cardLinkProtocols) { if ([p conformsToProtocol:@protocol(EpothekePrescriptionProtocol)]) { // found prescriptionProto self.prescriptionProtocol = p; - self.onAuthenticationCompletionCB(@[ [NSNull null], @"PrescriptionProtocol available" ] ); + self.onAuthenticationCompletionCB(@[ [NSNull null], [NSNull null] ] ); break; } } } } else { - self.onAuthenticationCompletionCB(@[ @"Authentication failed", [p0 getErrorMessage] ] ); + if ([[p0 getErrorMessage] rangeOfString:@"==>"].location == NSNotFound){ + self.onAuthenticationCompletionCB(@[ @"CLIENT_ERROR", [p0 getErrorMessage] ] ); + } else { + NSString *code = [[p0 getErrorMessage] componentsSeparatedByString:@" ==> "][0]; + NSString *msg = [[p0 getErrorMessage] componentsSeparatedByString:@" ==> "][1]; + self.onAuthenticationCompletionCB(@[ code, msg ] ); + } } } @@ -100,8 +106,11 @@ @interface CardLinkInterActionImp : NSObject @property RCTResponseSenderBlock onCardRecognizedCB; @property RCTResponseSenderBlock onCardRemovedCB; @property RCTResponseSenderBlock onCanRequestCB; +@property RCTResponseSenderBlock onCanRetryCB; @property RCTResponseSenderBlock onPhoneNumberRequestCB; +@property RCTResponseSenderBlock onPhoneNumberRetryCB; @property RCTResponseSenderBlock onSmsCodeRequestCB; +@property RCTResponseSenderBlock onSmsCodeRetryCB; @end @implementation CardLinkInterActionImp @@ -148,6 +157,26 @@ - (void)onCanRequest:(NSObject *)enterCan { } } +- (void)onCanRetry:(NSObject *)enterCan + withResultCode:(NSString*)resultCode + withErrorMessage:(NSString*)errorMessage { + RCTLogInfo(@"onCanRetry"); + if (enterCan && self.onCanRequestCB) { + self.userInputDispatch = ^(NSString *input) { + if ([enterCan conformsToProtocol:@protocol(ConfirmPasswordOperation)]) { + [enterCan confirmPassword:input]; + } + }; + if(!resultCode){ + resultCode = @"UNKNOWN_ERROR"; + } + if(!errorMessage){ + errorMessage = @"No detailed message available."; + } + self.onCanRetryCB(@[resultCode, errorMessage]); + } +} + - (void)onPhoneNumberRequest:(NSObject *)enterPhoneNumber { RCTLogInfo(@"onPhoneNumberRequest"); if (enterPhoneNumber && self.onPhoneNumberRequestCB) { @@ -159,6 +188,25 @@ - (void)onPhoneNumberRequest:(NSObject *)enterPhoneNumber self.onPhoneNumberRequestCB(nil); } } +- (void)onPhoneNumberRetry:(NSObject *)enterPhoneNumber + withResultCode:(NSString*)resultCode + withErrorMessage:(NSString*)errorMessage { + RCTLogInfo(@"onPhoneNumberRetry"); + if (enterPhoneNumber && self.onPhoneNumberRequestCB) { + self.userInputDispatch = ^(NSString *input) { + if ([enterPhoneNumber conformsToProtocol:@protocol(ConfirmTextOperation)]) { + [enterPhoneNumber confirmText:input]; + } + }; + if(!resultCode){ + resultCode = @"UNKNOWN_ERROR"; + } + if(!errorMessage){ + errorMessage = @"No detailed message available."; + } + self.onPhoneNumberRetryCB(@[resultCode,errorMessage]); + } +} - (void)onSmsCodeRequest:(NSObject *)smsCode { RCTLogInfo(@"onSmsCodeRequest"); @@ -171,6 +219,25 @@ - (void)onSmsCodeRequest:(NSObject *)smsCode { self.onSmsCodeRequestCB(nil); } } +- (void)onSmsCodeRetry:(NSObject *)smsCode + withResultCode:(NSString*)resultCode + withErrorMessage:(NSString*)errorMessage { + RCTLogInfo(@"onSmsCodeRetry"); + if (smsCode && self.onSmsCodeRequestCB) { + self.userInputDispatch = ^(NSString *input) { + if ([smsCode conformsToProtocol:@protocol(ConfirmPasswordOperation)]) { + [smsCode confirmPassword:input]; + } + }; + if(!resultCode){ + resultCode = @"UNKNOWN_ERROR"; + } + if(!errorMessage){ + errorMessage = @"No detailed message available."; + } + self.onSmsCodeRetryCB(@[resultCode, errorMessage]); + } +} @end @implementation RCTSdkModule @@ -234,6 +301,12 @@ @implementation RCTSdkModule } clInteraction.onCanRequestCB = cb; } +RCT_EXPORT_METHOD(set_cardlinkInteractionCB_onCanRetry : (RCTResponseSenderBlock)cb) { + if (!clInteraction) { + clInteraction = [CardLinkInterActionImp new]; + } + clInteraction.onCanRetryCB = cb; +} RCT_EXPORT_METHOD(set_cardlinkInteractionCB_onPhoneNumberRequest : (RCTResponseSenderBlock)cb) { if (!clInteraction) { @@ -241,6 +314,12 @@ @implementation RCTSdkModule } clInteraction.onPhoneNumberRequestCB = cb; } +RCT_EXPORT_METHOD(set_cardlinkInteractionCB_onPhoneNumberRetry : (RCTResponseSenderBlock)cb) { + if (!clInteraction) { + clInteraction = [CardLinkInterActionImp new]; + } + clInteraction.onPhoneNumberRetryCB = cb; +} RCT_EXPORT_METHOD(set_cardlinkInteractionCB_onSmsCodeRequest : (RCTResponseSenderBlock)cb) { if (!clInteraction) { @@ -248,6 +327,12 @@ @implementation RCTSdkModule } clInteraction.onSmsCodeRequestCB = cb; } +RCT_EXPORT_METHOD(set_cardlinkInteractionCB_onSmsCodeRetry : (RCTResponseSenderBlock)cb) { + if (!clInteraction) { + clInteraction = [CardLinkInterActionImp new]; + } + clInteraction.onSmsCodeRetryCB = cb; +} RCTResponseSenderBlock onSdkError; RCT_EXPORT_METHOD(set_sdkErrorCB : (RCTResponseSenderBlock)cb) { @@ -298,12 +383,13 @@ @implementation RCTSdkModule ); } -RCT_EXPORT_METHOD(startCardLink : (NSString *)cardLinkUrl) { +RCT_EXPORT_METHOD(startCardLink : (NSString *)cardLinkUrl tenantToken: (NSString *) tenantToken) { RCTLogInfo(@"onStarted: %@", cardLinkUrl); IOSNFCOptions *nfcOpts = [IOSNFCOptions new]; EpothekeSdkCore *sdk = [[EpothekeSdkCore alloc] initWithCardLinkUrl:cardLinkUrl + tenantToken:tenantToken cardLinkControllerCallback:clCtrlCB cardLinkInteractionProtocol:clInteraction sdkErrorHandler:errHandler diff --git a/gradle.properties b/gradle.properties index c9a5e52..ce78657 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,3 +13,4 @@ testHeapSize=1024m # project properties version=1.1.11-SNAPSHOT group=com.epotheke +kotlin.native.cacheKind.iosArm64=none diff --git a/libs.versions.toml b/libs.versions.toml index fda8376..982b9f9 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -1,25 +1,25 @@ [versions] agp = "8.2.+" -kotlin = "1.9.23" -kx-co = "1.8.+" -kx-serde = "1.6.+" +kotlin = "2.0.21" +kx-co = "1.9.+" +kx-serde = "1.7.+" ktor = "2.3.+" androidx-activityCompose = "1.9.+" androidx-appcompat = "1.6.+" androidx-constraintlayout = "2.1.+" -androidx-core-ktx = "1.12.+" +androidx-core-ktx = "1.13.+" androidx-espresso-core = "3.5.+" androidx-material = "1.11.+" androidx-test-junit = "1.1.+" -compose = "1.6.7" +compose = "1.6.11" compose-plugin = "1.6.2" kotlin-jackson = "2.17.0" -oec = "2.3.2" +oec = "2.3.5" quarkus = "3.10.1" @@ -50,6 +50,7 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-websocket = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } +ktor-client-auth = { module = "io.ktor:ktor-client-auth",version.ref = "ktor" } oec-android = { module = "org.openecard.clients:android-lib", version.ref = "oec" } diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts index 809ac62..b28c498 100644 --- a/sdk/build.gradle.kts +++ b/sdk/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { implementation(libs.ktor.client.websocket) implementation(libs.kotlin.coroutines.core) implementation(libs.kotlin.serialization.json) + implementation(libs.ktor.client.auth) } } val commonTest by getting { diff --git a/sdk/src/androidMain/kotlin/HttpClient.android.kt b/sdk/src/androidMain/kotlin/HttpClient.android.kt index 0f5fde2..dbbea3a 100644 --- a/sdk/src/androidMain/kotlin/HttpClient.android.kt +++ b/sdk/src/androidMain/kotlin/HttpClient.android.kt @@ -1,12 +1,23 @@ import io.ktor.client.* import io.ktor.client.plugins.websocket.* import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* -actual fun getHttpClient(): HttpClient { +actual fun getHttpClient(tenantToken: String?): HttpClient { return HttpClient(OkHttp) { install(WebSockets) { pingInterval = 15_000 } + tenantToken?.let{ + install(Auth) { + bearer { + loadTokens { + BearerTokens( it, "" ) + } + } + } + } } } diff --git a/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkActivity.android.kt b/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkActivity.android.kt index 6ce591d..f10e06f 100644 --- a/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkActivity.android.kt +++ b/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkActivity.android.kt @@ -45,6 +45,7 @@ abstract class SdkActivity : Activity() { epotheke = SdkCore( this, getCardLinkUrl(), + getTenantToken(), getControllerCallback(), getCardLinkInteraction(), getSdkErrorHandler(), @@ -79,6 +80,7 @@ abstract class SdkActivity : Activity() { } abstract fun getCardLinkUrl(): String + abstract fun getTenantToken(): String? abstract fun getControllerCallback(): CardLinkControllerCallback abstract fun getCardLinkInteraction(): CardLinkInteraction abstract fun getSdkErrorHandler(): SdkErrorHandler diff --git a/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkCore.android.kt b/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkCore.android.kt index bf507b6..cb1c741 100644 --- a/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkCore.android.kt +++ b/sdk/src/androidMain/kotlin/com/epotheke/sdk/SdkCore.android.kt @@ -40,6 +40,7 @@ private val logger = KotlinLogging.logger {} class SdkCore( private val ctx: Activity, private val cardLinkUrl: String, + private val tenantToken: String?, private val cardLinkControllerCallback: CardLinkControllerCallback, private val cardLinkInteraction: CardLinkInteraction, private val sdkErrorHandler: SdkErrorHandler @@ -66,7 +67,7 @@ class SdkCore( ctxManager = oec?.context(ctx) ctxManager?.initializeContext(object : StartServiceHandler { override fun onSuccess(actSource: ActivationSource) { - val websocket = WebsocketCommon(cardLinkUrl) + val websocket = WebsocketCommon(cardLinkUrl, tenantToken) val wsListener = WebsocketListenerCommon() val protocols = buildProtocols(websocket, wsListener) actSource.cardLinkFactory().create( @@ -130,6 +131,7 @@ class SdkCore( nfcIntentHelper?.disableNFCDispatch() needNfc = false cardLinkControllerCallback.onAuthenticationCompletion(p0, protocols) + destroyOecContext() } } } diff --git a/sdk/src/commonMain/kotlin/Websocket.common.kt b/sdk/src/commonMain/kotlin/Websocket.common.kt index c74f7de..e8626c3 100644 --- a/sdk/src/commonMain/kotlin/Websocket.common.kt +++ b/sdk/src/commonMain/kotlin/Websocket.common.kt @@ -66,14 +66,15 @@ open class WebsocketListenerCommon() : ChannelDispatcher { } } -expect fun getHttpClient(): HttpClient +expect fun getHttpClient(tenantToken: String?): HttpClient class WebsocketCommon( private var url: String, + private var tenantToken: String?, ) { private var wsListener: WiredWSListener? = null - private val client: HttpClient = getHttpClient() + private val client: HttpClient = getHttpClient(tenantToken) private var wsSession: DefaultClientWebSocketSession? = null diff --git a/sdk/src/commonMain/kotlin/com/epotheke/erezept/model/PrescriptionModel.common.kt b/sdk/src/commonMain/kotlin/com/epotheke/erezept/model/PrescriptionModel.common.kt index 7b7009f..61ecf85 100644 --- a/sdk/src/commonMain/kotlin/com/epotheke/erezept/model/PrescriptionModel.common.kt +++ b/sdk/src/commonMain/kotlin/com/epotheke/erezept/model/PrescriptionModel.common.kt @@ -22,6 +22,7 @@ package com.epotheke.erezept.model +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -395,6 +396,7 @@ val prescriptionModule = SerializersModule { } } +@OptIn(ExperimentalSerializationApi::class) val prescriptionJsonFormatter = Json { serializersModule = prescriptionModule; classDiscriminatorMode = ClassDiscriminatorMode.ALL_JSON_OBJECTS; @@ -408,11 +410,13 @@ object ByteArrayAsBase64Serializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ByteArrayAsBase64Serializer", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: ByteArray) { - val base64Encoded = Base64.encode(value).trimEnd('=') - encoder.encodeString(base64Encoded) + encoder.encodeString( + Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL).encode(value) + ) } override fun deserialize(decoder: Decoder): ByteArray { - return Base64.decode(decoder.decodeString()) + return Base64.withPadding(Base64.PaddingOption.ABSENT_OPTIONAL) + .decode(decoder.decodeString()) } } diff --git a/sdk/src/iosMain/kotlin/HttpClient.ios.kt b/sdk/src/iosMain/kotlin/HttpClient.ios.kt index 856ed33..8c60230 100644 --- a/sdk/src/iosMain/kotlin/HttpClient.ios.kt +++ b/sdk/src/iosMain/kotlin/HttpClient.ios.kt @@ -1,10 +1,21 @@ import io.ktor.client.* +import io.ktor.client.plugins.auth.* +import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.websocket.* -actual fun getHttpClient(): HttpClient { +actual fun getHttpClient(tenantToken: String?): HttpClient { return HttpClient() { install(WebSockets) { pingInterval = 15_000 } + tenantToken?.let{ + install(Auth) { + bearer { + loadTokens { + BearerTokens( it, "" ) + } + } + } + } } } diff --git a/sdk/src/iosMain/kotlin/SdkCore.ios.kt b/sdk/src/iosMain/kotlin/SdkCore.ios.kt index 334252c..646f7bb 100644 --- a/sdk/src/iosMain/kotlin/SdkCore.ios.kt +++ b/sdk/src/iosMain/kotlin/SdkCore.ios.kt @@ -2,6 +2,7 @@ import cocoapods.open_ecard.* import com.epotheke.sdk.CardLinkProtocol import com.epotheke.sdk.buildProtocols import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLoggingConfiguration import kotlinx.cinterop.ExperimentalForeignApi import platform.darwin.NSObject @@ -36,6 +37,7 @@ interface SdkErrorHandler { @OptIn(ExperimentalForeignApi::class) class SdkCore( private val cardLinkUrl: String, + private val tenantToken: String?, private val cardLinkControllerCallback: CardLinkControllerCallback, private val cardLinkInteractionProtocol: CardLinkInteractionProtocol, private val sdkErrorHandler: SdkErrorHandler, @@ -43,14 +45,34 @@ class SdkCore( ) { private var ctx: ContextManagerProtocol? = null private var activation: ActivationControllerProtocol? = null + private var dbgLogLevel = false +// private var logMessageHandler: LogMessageHandlerProtocol? = null + + fun setDebugLogLevel() { + dbgLogLevel = true + } + // fun setLogMessageHandler(handler: LogMessageHandlerProtocol){ + // logMessageHandler = handler + // } @OptIn(ExperimentalForeignApi::class) fun initCardLink() { logger.debug { cardLinkUrl } val oec = OpenEcardImp() - oec.developerOptions() + + if(dbgLogLevel){ + val devOpts = oec.developerOptions() + try{ + (devOpts as DeveloperOptionsProtocol).setDebugLogLevel() + } catch (e: Exception){ + logger.warn { "Could not set loglevel to DEBUG" } + } + // if(logMessageHandler != null) { + // oec.developerOptions().registerLogHandler(logMessageHandler) + // } + } ctx = oec.context(nfcOpts as NSObject) as ContextManagerProtocol - ctx!!.initializeContext(object : StartServiceHandlerProtocol, NSObject() { + ctx?.initializeContext(object : StartServiceHandlerProtocol, NSObject() { override fun onFailure(response: NSObject?) { println("Fail") sdkErrorHandler.hdl(response) @@ -59,32 +81,32 @@ class SdkCore( override fun onSuccess(source: NSObject?) { val src = source as ActivationSourceProtocol val factory = src.cardLinkFactory() as CardLinkControllerFactoryProtocol - val ws = WebsocketCommon(cardLinkUrl) + val ws = WebsocketCommon(cardLinkUrl, tenantToken) val wsListener = WebsocketListenerCommon() val protocols = buildProtocols(ws, wsListener) activation = factory.create( - WebsocketIos(ws), - withActivation = OverridingControllerCallback(protocols, cardLinkControllerCallback) as NSObject, + WebsocketIos(ws, sdkErrorHandler), + withActivation = OverridingControllerCallback(this@SdkCore, protocols, cardLinkControllerCallback) as NSObject, withInteraction = cardLinkInteractionProtocol as NSObject, withListenerSuccessor = WebsocketListenerIos(wsListener) as NSObject, ) as ActivationControllerProtocol - } - } as NSObject) } fun terminateContext() { - ctx!!.terminateContext(object : StopServiceHandlerProtocol, NSObject() { + if(activation != null){ + (activation as ActivationControllerProtocol).cancelOngoingAuthentication() + } + ctx?.terminateContext(object : StopServiceHandlerProtocol, NSObject() { override fun onFailure(response: NSObject?) { - logger.debug { "stopped successfully" } + logger.warn { "Cardlink sdk stopped with error: ${(response as ServiceErrorResponseProtocol).getErrorMessage()}" } } override fun onSuccess() { - logger.debug { "stopped with error" } - sdkErrorHandler.hdl(null) + logger.debug { "Cardlink sdk stopped successfully" } } } as NSObject) @@ -93,11 +115,14 @@ class SdkCore( @OptIn(ExperimentalForeignApi::class) private class OverridingControllerCallback( + val sdk: SdkCore, val protocols: Set, val cardLinkControllerCallback: CardLinkControllerCallback ) : ControllerCallbackProtocol, NSObject() { override fun onAuthenticationCompletion(result: NSObject?) { cardLinkControllerCallback.onAuthenticationCompletion(result as ActivationResultProtocol, protocols) + logger.warn { "PROCESS ENDED calling terminate" } + sdk.terminateContext() } override fun onStarted() { diff --git a/sdk/src/iosMain/kotlin/Websocket.ios.kt b/sdk/src/iosMain/kotlin/Websocket.ios.kt index f62c8bd..61410da 100644 --- a/sdk/src/iosMain/kotlin/Websocket.ios.kt +++ b/sdk/src/iosMain/kotlin/Websocket.ios.kt @@ -20,16 +20,26 @@ * ***************************************************************************/ +import cocoapods.open_ecard.ServiceErrorCode +import cocoapods.open_ecard.ServiceErrorResponseProtocol import cocoapods.open_ecard.WebsocketProtocol import cocoapods.open_ecard.WebsocketListenerProtocol +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.client.plugins.websocket.* import kotlinx.cinterop.ExperimentalForeignApi import platform.darwin.NSObject +private val logger = KotlinLogging.logger {} + @OptIn(ExperimentalForeignApi::class) -fun createWs( - url: String, -): WebsocketProtocol { - return WebsocketIos(WebsocketCommon(url)) +class SockError(val errMessage: String?) : ServiceErrorResponseProtocol, NSObject() { + override fun getErrorMessage(): String? { + return errMessage + } + + override fun getStatusCode(): ServiceErrorCode { + return cocoapods.open_ecard.ServiceErrorCode.kServiceErrorCodeINTERNAL_ERROR + } } @OptIn(ExperimentalForeignApi::class) @@ -44,16 +54,23 @@ private class WiredWSListenerImplementation constructor( } @OptIn(ExperimentalForeignApi::class) -class WebsocketListenerIos(private val wsListenerCommon: WebsocketListenerCommon) : WebsocketListenerProtocol, NSObject() { +class WebsocketListenerIos(private val wsListenerCommon: WebsocketListenerCommon) : WebsocketListenerProtocol, + NSObject() { override fun onOpen(webSocket: NSObject?) = wsListenerCommon.onOpen() - override fun onClose(webSocket: NSObject?, withStatusCode: Int, withReason: String?) = wsListenerCommon.onClose(withStatusCode, withReason) - override fun onError(webSocket: NSObject?, withError: String?) = wsListenerCommon.onError(withError ?: "unknown error") - override fun onText(webSocket: NSObject?, withData: String?) = wsListenerCommon.onText(withData ?: "invalid message") + override fun onClose(webSocket: NSObject?, withStatusCode: Int, withReason: String?) = + wsListenerCommon.onClose(withStatusCode, withReason) + + override fun onError(webSocket: NSObject?, withError: String?) = + wsListenerCommon.onError(withError ?: "unknown error") + + override fun onText(webSocket: NSObject?, withData: String?) = + wsListenerCommon.onText(withData ?: "invalid message") } @OptIn(ExperimentalForeignApi::class) class WebsocketIos( private val commonWS: WebsocketCommon, + private val errorHandler: SdkErrorHandler, ) : NSObject(), WebsocketProtocol { @@ -69,31 +86,44 @@ class WebsocketIos( // @Throws(WebsocketException::class) override fun connect() { -// try { - commonWS.connect() - // } catch (e : Exception){ - // throw WebsocketException(e.message) - // } + try { + commonWS.connect() + } catch (e: Exception) { + + logger.warn(e) {"Websocket connection failed."} +// errorHandler.hdl( +// SockError("Error during WS connect: ${e.message}. Is tenantToken valid?") +// as NSObject +// ) + } } // @Throws(WebsocketException::class) override fun close(statusCode: Int, withReason: String?) { - // try { - commonWS.close(statusCode, withReason) - // } catch (e : Exception){ - // throw WebsocketException(e.message) - // } + try { + commonWS.close(statusCode, withReason) + } catch (e: Exception) { + logger.warn(e) {"Websocket close failed."} + //errorHandler.hdl( + // SockError("Error during WS close: ${e.message}.") + // as NSObject + //) + } } // @Throws(WebsocketException::class) override fun send(data: String?) { - // try{ - data?.let { - commonWS.send(data) + try { + data?.let { + commonWS.send(data) + } + } catch (e: Exception) { + logger.warn(e) {"Websocket close failed."} + //errorHandler.hdl( + // SockError("Error during WS send: ${e.message}.") + // as NSObject + //) } - // } catch (e : Exception){ - // throw WebsocketException(e.message) - // } } override fun isClosed(): Boolean {