diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..4515aa3 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index e497da9..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 604522b..9927a23 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + compose_version = '1.1.0-beta01' + } ext.kotlin_version = "1.6.10" repositories { google() diff --git a/quickstart-kotlin/build.gradle b/quickstart-kotlin/build.gradle index 0ea98e9..d7c32d4 100644 --- a/quickstart-kotlin/build.gradle +++ b/quickstart-kotlin/build.gradle @@ -14,6 +14,9 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } } buildTypes { @@ -37,6 +40,17 @@ android { kotlinOptions { jvmTarget = "1.8" } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } } dependencies { @@ -54,4 +68,12 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0' implementation 'com.google.android.material:material:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.3.1' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" } diff --git a/quickstart-kotlin/src/main/AndroidManifest.xml b/quickstart-kotlin/src/main/AndroidManifest.xml index 328f4b1..5d89411 100644 --- a/quickstart-kotlin/src/main/AndroidManifest.xml +++ b/quickstart-kotlin/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + package="com.paypal.checkoutsamples"> - + + - + - + + android:label="@string/orders_quick_start_activity_title"/> + android:label="@string/payment_button_quick_start_activity_title"/> + android:label="@string/token_quick_start_activity_configuration_options_title"/> - + \ No newline at end of file diff --git a/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/compose/KotlinJetpackComposeQuickStart.kt b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/compose/KotlinJetpackComposeQuickStart.kt new file mode 100644 index 0000000..4af5972 --- /dev/null +++ b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/order/compose/KotlinJetpackComposeQuickStart.kt @@ -0,0 +1,193 @@ +package com.paypal.checkoutsamples.order.compose + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.paypal.checkout.PayPalCheckout +import com.paypal.checkout.approve.OnApprove +import com.paypal.checkout.cancel.OnCancel +import com.paypal.checkout.createorder.CreateOrder +import com.paypal.checkout.createorder.CurrencyCode +import com.paypal.checkout.createorder.OrderIntent +import com.paypal.checkout.createorder.ShippingPreference +import com.paypal.checkout.createorder.UserAction +import com.paypal.checkout.error.OnError +import com.paypal.checkout.order.CaptureOrderResult +import com.paypal.checkoutsamples.order.CreatedItem +import com.paypal.checkoutsamples.order.compose.checkoutstate.Loading +import com.paypal.checkoutsamples.order.compose.checkoutstate.OrderCapturingFailed +import com.paypal.checkoutsamples.order.compose.checkoutstate.OrderPaidForSuccessfully +import com.paypal.checkoutsamples.order.compose.checkoutstate.OrderPaymentCancelled +import com.paypal.checkoutsamples.order.compose.checkoutstate.OrderPaymentFailedWithAnError +import com.paypal.checkoutsamples.order.compose.checkoutstate.PaypalCheckoutState +import com.paypal.checkoutsamples.order.usecase.CreateOrderRequest +import com.paypal.checkoutsamples.order.usecase.CreateOrderUseCase +import com.paypal.checkoutsamples.ui.theme.AndroidNativeCheckoutSamplesTheme +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class KotlinJetpackComposeQuickStart : ComponentActivity() { + private val checkoutState = MutableStateFlow(Loading) + private val checkOutSdk:PayPalCheckout + get() = PayPalCheckout + private val createdItems = mutableListOf() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + registerPaypalCheckoutCallback(checkOutSdk) + val createOrderUseCase by lazy { CreateOrderUseCase() } + + setContent { + AndroidNativeCheckoutSamplesTheme { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + PaymentScreen(paypalSdkInstance = checkOutSdk, orderUseCase = createOrderUseCase, + checkoutState = checkoutState, + createdItems = createdItems) + } + } + } + } + + + + fun registerPaypalCheckoutCallback(checkoutSdk: PayPalCheckout) { + checkoutSdk.registerCallbacks(onApprove = OnApprove { approval -> + approval.orderActions.capture { result -> + val message = when (result) { + is CaptureOrderResult.Success -> { + checkoutState.update { OrderPaidForSuccessfully } + "payment successful. Thank you for shopping with us" + } + + is CaptureOrderResult.Error -> { + checkoutState.update { OrderCapturingFailed } + "failed to capture your order" + } + } + Log.i("ComposeActivityTag",message) + } + }, onCancel = OnCancel { + checkoutState.update { OrderPaymentCancelled } + Log.i("ComposeActivityTag","payment cancelled") + }, onError = OnError { errorInfo -> + checkoutState.update { OrderPaymentFailedWithAnError(errorInfo.reason) } + Log.i("ComposeActivityTag","the following error occurred while paying ${errorInfo.reason}") + }) + } + @Composable + fun PaymentScreen(modifier: Modifier=Modifier, + paypalSdkInstance:PayPalCheckout, + orderUseCase: CreateOrderUseCase, + checkoutState:StateFlow, + createdItems: List){ + var observeCheckoutState by remember { mutableStateOf(false) } + var checkoutStateMsg by remember { mutableStateOf("") } + val internalCheckoutState by checkoutState.collectAsState(Loading) + when(observeCheckoutState){ + true->{ + CircularProgressIndicator(modifier = Modifier.size(200.dp)) + ObservePaypalCheckoutState(internalCheckoutState){ + observeState,msg-> + observeCheckoutState = observeState + checkoutStateMsg=msg + } + } + else->{ + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center){ + Column(modifier = Modifier, verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally){ + Spacer(Modifier.height(20.dp)) + AnimatedVisibility(visible = checkoutStateMsg.isNotEmpty()){ + Text(checkoutStateMsg, textAlign = TextAlign.Center) + } + Spacer(Modifier.height(20.dp)) + Button(modifier=Modifier.fillMaxWidth() + .padding(horizontal = 20.dp), + onClick = { + // initiate payment + observeCheckoutState=true + initiatePaymentMethod(paypalSdkInstance, orderUseCase, createdItems) + }){ + Text("Complete Purchase") + } + } + } + } + } + } + /* Observes the different check-out states and updates the screen with the correct message + * Can be customized according to the user-needs */ + @Composable + private fun ObservePaypalCheckoutState(checkoutState: PaypalCheckoutState, + updateObserveCheckoutState:(Boolean,String)->Unit){ + LaunchedEffect(checkoutState){ + when(checkoutState){ + Loading -> Log.d("ComposeActivityTag","Initiating order capture..") + OrderCapturingFailed -> { + // stop observing and show the default view to enable the user to initiate payment again + updateObserveCheckoutState(false,"Order capturing failed..") + Log.d("ComposeActivityTag","Order capturing failed..") + } + OrderPaidForSuccessfully ->{ + Log.d("ComposeActivityTag","Order capturing success") + // stop observing and show the default view to enable the user to initiate payment again + updateObserveCheckoutState(false, "Order capturing success") + } + OrderPaymentCancelled ->{ + Log.d("ComposeActivityTag","Order payment cancelled!") + updateObserveCheckoutState(false,"Order payment cancelled!") + } + is OrderPaymentFailedWithAnError ->{ + updateObserveCheckoutState(false,"Order payment failed with the following error ${checkoutState.errorMsg}") + Log.d("ComposeActivityTag","Order payment failed with the following error ${checkoutState.errorMsg}") + } + } + } + } + /* Creates the order item needed by paypal checkout sdk */ + private fun initiatePaymentMethod( + paypalSdkInstance: PayPalCheckout, + orderUseCase: CreateOrderUseCase, + createdItems: List + ) { + // create your order request + val createOrderRequest = CreateOrderRequest( + orderIntent = OrderIntent.CAPTURE, + userAction = UserAction.PAY_NOW, shippingPreference = ShippingPreference.NO_SHIPPING, + currencyCode = CurrencyCode.USD, createdItems = createdItems + ) + val orderObject = orderUseCase.execute(createOrderRequest) + // start check out process + paypalSdkInstance.startCheckout(createOrder = CreateOrder.invoke { actions -> + actions.create(orderObject) { orderId -> + Log.d("ComposeActivityTag", "Order id $orderId") + } + }) + } +} diff --git a/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Color.kt b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Color.kt new file mode 100644 index 0000000..b14e173 --- /dev/null +++ b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package com.paypal.checkoutsamples.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Shape.kt b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Shape.kt new file mode 100644 index 0000000..5fab9a5 --- /dev/null +++ b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.paypal.checkoutsamples.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Theme.kt b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Theme.kt new file mode 100644 index 0000000..bd903c2 --- /dev/null +++ b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Theme.kt @@ -0,0 +1,44 @@ +package com.paypal.checkoutsamples.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun AndroidNativeCheckoutSamplesTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Type.kt b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Type.kt new file mode 100644 index 0000000..a8d1e4f --- /dev/null +++ b/quickstart-kotlin/src/main/java/com/paypal/checkoutsamples/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package com.paypal.checkoutsamples.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/quickstart-kotlin/src/main/res/values/strings.xml b/quickstart-kotlin/src/main/res/values/strings.xml index 9cb3d43..cf8ca80 100644 --- a/quickstart-kotlin/src/main/res/values/strings.xml +++ b/quickstart-kotlin/src/main/res/values/strings.xml @@ -24,25 +24,45 @@ Shipping Preference: Get From File No Shipping - Set Provided Address + Set Provided Address + Enable Shipping Callbacks? Place Order x%1$s Payment Button Payment Button - To get a better understanding for how Payment Buttons work within the Checkout SDK, please observe the logs which are printed in Logcat, filtering on PaymentButtonQuickStartActivity.\n\nTo see other buttons or customize this one, please view the activity_payment_button_quick_start layout file. + To get a better understanding for how + Payment Buttons work within the Checkout SDK, please observe the logs which are printed in Logcat, filtering on + PaymentButtonQuickStartActivity.\n\nTo see other buttons or customize this one, please view the + activity_payment_button_quick_start + layout file. + Configure Server Side Integration - @string/orders_quick_start_activity_configuration_user_action_label - @string/orders_quick_start_activity_configuration_user_action_option_continue - @string/orders_quick_start_activity_configuration_user_action_option_pay_now - @string/orders_quick_start_activity_configuration_intent_label - @string/orders_quick_start_activity_configuration_intent_option_capture - @string/orders_quick_start_activity_configuration_intent_option_authorize + + @string/orders_quick_start_activity_configuration_user_action_label + + + @string/orders_quick_start_activity_configuration_user_action_option_continue + + + @string/orders_quick_start_activity_configuration_user_action_option_pay_now + + + @string/orders_quick_start_activity_configuration_intent_label + + + @string/orders_quick_start_activity_configuration_intent_option_capture + + + @string/orders_quick_start_activity_configuration_intent_option_authorize + Total Amount Total Amount Required - @string/orders_quick_start_activity_submit_order_button + + @string/orders_quick_start_activity_submit_order_button + Create An Item Item Name @@ -55,5 +75,6 @@ Create Item Item Category is required Required + KotlinJetpackComposeQuickStart diff --git a/quickstart-kotlin/src/main/res/values/themes.xml b/quickstart-kotlin/src/main/res/values/themes.xml new file mode 100644 index 0000000..b153184 --- /dev/null +++ b/quickstart-kotlin/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file