Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-29 10:11:15 +02:00
parent 1b706b20a7
commit 72c9e20396
19 changed files with 934 additions and 127 deletions

View file

@ -91,6 +91,7 @@
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
<string name="addfunds_buy_row_description">Credit card or bank account</string>
<string name="addfunds_fund_token">Fund token</string>
<string name="addfunds_receive_row_description">Share your address or QR-code</string>
<string name="addfunds_swap_row_description">Between your portfolios</string>
<string name="addfunds_you_receive_title">You receive</string>
@ -1213,9 +1214,7 @@
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="provider_name_support">%s support</string>
<string name="push_notification_settings_banner_button_grant_permission">Grant permission</string>
<string name="push_notification_settings_banner_description">Push Notifications are enabled but won\'t work until you allow notifications in your device settings</string>
<string name="push_notification_settings_banner_description_grant_permission">Push Notifications are enabled but won\'t work until you grant permission</string>
<string name="push_notification_settings_banner_description">Push Notifications are enabled but won\'t work until you allow them</string>
<string name="push_notification_settings_banner_title">Allow notifications</string>
<string name="push_notification_settings_offers_updates_subtitle">Product news, exclusive offers, and activity reminders.</string>
<string name="push_notification_settings_offers_updates_title">Offers &amp; Updates</string>
@ -1687,11 +1686,13 @@
<string name="tangem_pay_freeze_card_failed">Failed to freeze the card. Try again later.</string>
<string name="tangem_pay_freeze_card_freeze">Freeze</string>
<string name="tangem_pay_freeze_card_success">Your card is frozen.</string>
<string name="tangem_pay_freeze_card_unfreeze">Unfreeze</string>
<string name="tangem_pay_get_help">Get Help</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Reason: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Other</string>
<string name="tangem_pay_pin_code_title">PIN-code</string>
<string name="tangem_pay_rooted_device_subtitle">Unable to use on rooted devices</string>
<string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string>
@ -1742,7 +1743,6 @@
<string name="tangempay_card_details_open_wallet_title">Add card to Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Add card to Apple Pay</string>
<string name="tangempay_card_details_pin_code">PIN code</string>
<string name="tangempay_card_details_receive_description">Share your address or show QR-code</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
<string name="tangempay_card_details_reissue_card">Replace card</string>
@ -1751,7 +1751,6 @@
<string name="tangempay_card_details_rename_card_placeholder">Card name</string>
<string name="tangempay_card_details_reveal_text">Reveal</string>
<string name="tangempay_card_details_show_details">Show details</string>
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unable_to_rename_card_description">Please try again later</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
@ -1769,6 +1768,7 @@
<string name="tangempay_card_page_daily_limit_change">Change</string>
<string name="tangempay_card_page_daily_limit_current_limit">Current limit</string>
<string name="tangempay_card_page_daily_limit_error_description">We couldn\'t load your daily limit. Please try again.</string>
<string name="tangempay_card_page_daily_limit_error_subtitle">Reload to try again</string>
<string name="tangempay_card_page_daily_limit_error_title">Daily limit unavailable</string>
<string name="tangempay_card_page_daily_limit_success_description">You can change it again anytime you like</string>
<string name="tangempay_card_page_daily_limit_success_title">Daily limit is set</string>
@ -1854,7 +1854,6 @@
<string name="tangempay_sync_needed">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_body">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_button">Renew session</string>
<string name="tangempay_sync_needed_restore_access">Renew session</string>
<string name="tangempay_sync_needed_title">Payment account session expired</string>
<string name="tangempay_tangem_visa_card">Use USDC for everyday payments</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unreachable</string>
@ -1864,7 +1863,6 @@
<string name="tangempay_topup_swap_body">Use crypto from your wallet to top up your payment account</string>
<string name="tangempay_topup_swap_title">Swap from Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC on Polygon network</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Click the button below to restore access</string>
<string name="tangempay_withdrawal_note_description">Funds from refunded purchases wont be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases</string>
<string name="tangempay_withdrawal_note_title">Please note</string>
<string name="tangempay_your_pin_code">Your PIN code</string>

View file

@ -0,0 +1,33 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M16.002,8.499C16.554,8.499 17.002,8.947 17.002,9.499V14.501C17.002,15.053 16.554,15.501 16.002,15.501C15.45,15.501 15.002,15.053 15.002,14.501V9.499C15.002,8.947 15.45,8.499 16.002,8.499Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M6.715,10.933C7.255,10.493 8.05,10.525 8.554,11.027C8.947,11.42 9.065,12.012 8.853,12.526C8.64,13.04 8.138,13.375 7.582,13.375C7.026,13.375 6.524,13.04 6.312,12.526C6.099,12.012 6.217,11.42 6.61,11.027L6.715,10.933Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M11.133,10.933C11.673,10.493 12.468,10.525 12.972,11.027C13.365,11.42 13.483,12.012 13.271,12.526C13.058,13.04 12.556,13.375 12,13.375C11.444,13.375 10.942,13.04 10.729,12.526C10.517,12.012 10.635,11.42 11.028,11.027L11.133,10.933Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M12,2C17.523,2 22,6.477 22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2ZM12,4C7.582,4 4,7.582 4,12C4,16.418 7.582,20 12,20C16.418,20 20,16.418 20,12C20,7.582 16.418,4 12,4Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M3.72,3.719C4.012,3.427 4.487,3.427 4.78,3.719L10,8.942L15.219,3.723C15.512,3.431 15.987,3.43 16.28,3.723C16.573,4.016 16.572,4.491 16.28,4.784L11.061,10.002L16.28,15.219C16.573,15.512 16.572,15.987 16.28,16.28C15.987,16.573 15.512,16.573 15.219,16.281L10,11.063L4.78,16.284C4.487,16.576 4.012,16.576 3.72,16.284C3.427,15.991 3.427,15.516 3.72,15.223L8.939,10.002L3.72,4.78C3.427,4.487 3.427,4.012 3.72,3.719Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,27 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10,3C10.414,3 10.75,3.336 10.75,3.75C10.75,4.164 10.414,4.5 10,4.5H7.222C5.719,4.5 4.5,5.719 4.5,7.222V12.777C4.5,14.281 5.719,15.499 7.222,15.499H12.777C14.281,15.499 15.499,14.281 15.499,12.777V10C15.499,9.586 15.835,9.25 16.249,9.25C16.663,9.25 16.999,9.586 16.999,10V12.777C16.999,15.109 15.109,16.999 12.777,16.999H7.222C4.89,16.999 3,15.109 3,12.777V7.222C3,4.89 4.89,3 7.222,3H10Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M14.704,3C15.313,3.001 15.896,3.243 16.326,3.674C16.757,4.105 16.999,4.689 16.999,5.298C16.999,5.907 16.757,6.491 16.326,6.922C16.319,6.929 16.31,6.935 16.303,6.942H16.305C16.308,6.939 16.313,6.935 16.315,6.933L15.834,7.414C15.532,7.716 15.118,8.129 14.672,8.575C13.779,9.468 12.755,10.492 12.229,11.019C11.881,11.366 11.427,11.587 10.939,11.646L9.861,11.78C9.406,11.836 8.951,11.677 8.63,11.35C8.309,11.022 8.158,10.564 8.223,10.11L8.382,8.993C8.448,8.522 8.666,8.085 9.003,7.748C9.74,7.011 11.541,5.21 13.079,3.672C13.51,3.241 14.095,2.999 14.704,3ZM15.266,4.733C15.117,4.584 14.913,4.5 14.702,4.5C14.518,4.5 14.34,4.564 14.198,4.68L14.14,4.732C12.601,6.27 10.801,8.071 10.064,8.809C9.958,8.915 9.888,9.054 9.867,9.203V9.206L9.713,10.286L10.755,10.158H10.757C10.912,10.139 11.057,10.069 11.168,9.958L15.269,5.857C15.416,5.708 15.499,5.507 15.499,5.298C15.499,5.087 15.415,4.883 15.266,4.733Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,24 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,2C12.552,2 13,2.448 13,3V4.175L13.461,3.714C13.851,3.324 14.485,3.324 14.875,3.714C15.266,4.104 15.266,4.737 14.875,5.128L13,7.003V10.267L15.828,8.633L16.514,6.074C16.657,5.541 17.205,5.224 17.739,5.367C18.272,5.51 18.588,6.058 18.445,6.592L18.277,7.22L19.5,6.514C19.978,6.238 20.59,6.402 20.866,6.88C21.142,7.358 20.978,7.97 20.5,8.246L19.276,8.952L19.906,9.122C20.44,9.265 20.756,9.813 20.614,10.347C20.471,10.88 19.922,11.196 19.389,11.054L16.826,10.367L13.999,11.999L16.826,13.632L19.389,12.946C19.922,12.804 20.471,13.12 20.614,13.653C20.756,14.187 20.44,14.735 19.906,14.878L19.276,15.046L20.5,15.754C20.978,16.03 21.142,16.642 20.866,17.12C20.59,17.598 19.978,17.762 19.5,17.486L18.277,16.779L18.445,17.408C18.588,17.941 18.272,18.49 17.739,18.633C17.205,18.776 16.657,18.459 16.514,17.926L15.828,15.365L13,13.732V16.996L14.875,18.872C15.266,19.263 15.266,19.896 14.875,20.286C14.485,20.676 13.851,20.677 13.461,20.286L13,19.824V21C13,21.552 12.552,22 12,22C11.448,22 11,21.552 11,21V19.824L10.539,20.286C10.149,20.676 9.516,20.676 9.125,20.286C8.735,19.896 8.735,19.263 9.125,18.872L11,16.996V13.732L8.172,15.364L7.487,17.926C7.345,18.459 6.796,18.775 6.263,18.633C5.73,18.49 5.413,17.942 5.556,17.408L5.724,16.779L4.5,17.486C4.022,17.762 3.41,17.598 3.134,17.12C2.858,16.642 3.022,16.03 3.5,15.754L4.724,15.046L4.095,14.878C3.562,14.735 3.245,14.187 3.388,13.653C3.531,13.12 4.079,12.803 4.612,12.946L7.173,13.632L9.999,11.999L7.174,10.367L4.612,11.054C4.079,11.197 3.531,10.88 3.388,10.347C3.245,9.813 3.561,9.265 4.095,9.122L4.724,8.953L3.5,8.246C3.022,7.97 2.858,7.358 3.134,6.88C3.41,6.402 4.022,6.238 4.5,6.514L5.724,7.22L5.556,6.592C5.413,6.058 5.73,5.51 6.263,5.367C6.796,5.225 7.345,5.541 7.487,6.074L8.172,8.634L11,10.267V7.003L9.125,5.128C8.735,4.737 8.735,4.104 9.125,3.714C9.516,3.324 10.149,3.324 10.539,3.714L11,4.175V3C11,2.448 11.448,2 12,2Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,27 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M4.34,4.367C7.465,1.211 12.535,1.211 15.66,4.367C18.779,7.519 18.779,12.627 15.66,15.78C15.368,16.074 14.892,16.077 14.598,15.786C14.304,15.495 14.302,15.02 14.593,14.725C17.135,12.157 17.135,7.99 14.593,5.422C12.055,2.858 7.943,2.858 5.406,5.422C2.864,7.99 2.864,12.157 5.406,14.725C5.697,15.02 5.695,15.495 5.401,15.786C5.106,16.077 4.632,16.074 4.34,15.78C1.22,12.627 1.22,7.52 4.34,4.367Z"
android:fillColor="#0090F9"/>
<path
android:pathData="M12.121,6.724C12.413,6.431 12.888,6.428 13.182,6.719C13.476,7.01 13.479,7.485 13.188,7.779L10.463,10.531C10.172,10.825 9.697,10.827 9.403,10.536C9.108,10.245 9.106,9.77 9.397,9.475L12.121,6.724Z"
android:fillColor="#0090F9"/>
</vector>

View file

@ -0,0 +1,27 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.57,14C10.123,14 10.57,14.448 10.57,15C10.57,15.552 10.123,16 9.57,16H7C6.448,16 6,15.552 6,15C6,14.448 6.448,14 7,14H9.57Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M18,4C20.209,4 22,5.791 22,8V16C22,18.209 20.209,20 18,20H6C3.791,20 2,18.209 2,16V8C2,5.791 3.791,4 6,4H18ZM4,16C4,17.105 4.895,18 6,18H18C19.105,18 20,17.105 20,16V10H4V16ZM6,6C4.895,6 4,6.895 4,8H20C20,6.895 19.105,6 18,6H6Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,30 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10.096,12.376C10.571,12.424 10.945,12.825 10.945,13.315C10.945,13.837 10.522,14.26 10,14.261C9.478,14.26 9.056,13.837 9.056,13.315C9.055,12.791 9.481,12.371 10,12.371L10.096,12.376Z"
android:fillColor="#E68A03"/>
<path
android:pathData="M10,5.739C10.414,5.74 10.75,6.075 10.75,6.489V10.39C10.75,10.804 10.414,11.139 10,11.14C9.586,11.139 9.25,10.804 9.25,10.39V6.489C9.25,6.075 9.586,5.739 10,5.739Z"
android:fillColor="#E68A03"/>
<path
android:pathData="M12.554,2.002C13.179,2.002 13.78,2.251 14.223,2.693L17.305,5.775C17.749,6.218 17.998,6.819 17.998,7.445V12.554C17.998,13.179 17.749,13.78 17.307,14.223L13.752,17.778C13.611,17.919 13.42,17.998 13.221,17.998H7.445C6.82,17.998 6.219,17.749 5.776,17.307L2.693,14.224C2.251,13.781 2.002,13.181 2.002,12.555V7.445C2.002,6.819 2.251,6.219 2.693,5.776L5.776,2.693C6.219,2.251 6.819,2.002 7.445,2.002H12.554ZM7.445,3.502C7.218,3.502 6.998,3.593 6.837,3.754L3.754,6.837C3.593,6.998 3.502,7.218 3.502,7.445V12.555C3.502,12.782 3.593,13.002 3.754,13.163L6.837,16.245C6.998,16.407 7.218,16.498 7.445,16.498H12.91L16.245,13.162C16.407,13.001 16.498,12.781 16.498,12.554V7.445C16.498,7.218 16.407,6.999 16.245,6.838L13.162,3.754C13.001,3.593 12.781,3.502 12.554,3.502H7.445Z"
android:fillColor="#E68A03"/>
</vector>

View file

@ -1,13 +1,18 @@
package com.tangem.features.tangempay.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.tangempay.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.R as CoreUiR
@Immutable
internal data class TangemPayCardPageUM(
val settings: ImmutableList<TangemPayCardPageSetting>,
val settingsV2: ImmutableList<TangemPayCardPageSettingV2>,
val onBackClick: () -> Unit,
val dailyLimitState: TangemPayDailyLimitBlockState,
val addToWalletBlockState: AddToWalletBlockState? = null,
@ -15,7 +20,11 @@ internal data class TangemPayCardPageUM(
) {
companion object {
fun stub(
addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(onClick = {}, onClickClose = {}),
addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(
onClick = {},
onClickClose = {},
shouldUseMagicEffect = false,
),
settings: ImmutableList<TangemPayCardPageSetting> = persistentListOf(
TangemPayCardPageSetting(TextReference.Str("Pin Code")) {},
TangemPayCardPageSetting(TextReference.Str("Freeze Card")) {},
@ -23,9 +32,11 @@ internal data class TangemPayCardPageUM(
),
isReissueInProgress: Boolean = false,
dailyLimitState: TangemPayDailyLimitBlockState = TangemPayDailyLimitBlockState.Content.stub(),
settingsV2: ImmutableList<TangemPayCardPageSettingV2> = TangemPayCardPageSettingV2.stubList(),
) = TangemPayCardPageUM(
addToWalletBlockState = addToWalletBlockState,
settings = settings,
settingsV2 = settingsV2,
onBackClick = {},
isReissueInProgress = isReissueInProgress,
dailyLimitState = dailyLimitState,
@ -38,4 +49,49 @@ internal data class TangemPayCardPageSetting(
val title: TextReference,
val testTag: String? = null,
val onSettingClick: () -> Unit,
)
)
@Immutable
internal data class TangemPayCardPageSettingV2(
val id: Id,
val title: TextReference,
val isLoading: Boolean = false,
val isEnabled: Boolean = true,
val testTag: String? = null,
val onClick: () -> Unit,
@param:DrawableRes val iconRes: Int,
) {
enum class Id {
Details, Freeze, ChangePin
}
companion object {
fun stubList(isFrozen: Boolean = false): ImmutableList<TangemPayCardPageSettingV2> = persistentListOf(
TangemPayCardPageSettingV2(
id = Id.Details,
title = resourceReference(R.string.details_title),
onClick = {},
iconRes = CoreUiR.drawable.ic_visa_card_details_24,
),
TangemPayCardPageSettingV2(
id = Id.Freeze,
title = resourceReference(
if (isFrozen) {
R.string.tangem_pay_freeze_card_unfreeze
} else {
R.string.tangem_pay_freeze_card_freeze
},
),
onClick = {},
iconRes = CoreUiR.drawable.ic_freeze_24,
),
TangemPayCardPageSettingV2(
id = Id.ChangePin,
title = resourceReference(R.string.tangem_pay_pin_code_title),
onClick = {},
iconRes = CoreUiR.drawable.ic_card_pin_24,
),
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
@ -34,6 +35,7 @@ internal data class TangemPayCardDetailsUM(
val isActionsAvailable: Boolean = false,
)
@Immutable
internal sealed interface DisplayNameState {
val displayName: String
@ -61,6 +63,7 @@ internal sealed interface DisplayNameState {
}
}
@Immutable
internal sealed class TangemPayDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<ActionButtonConfig>
@ -94,4 +97,5 @@ internal sealed class TangemPayDetailsBalanceBlockState {
internal data class AddToWalletBlockState(
val onClick: () -> Unit,
val onClickClose: () -> Unit,
val shouldUseMagicEffect: Boolean,
)

View file

@ -66,7 +66,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
displayName = card.displayName,
isEditingNameEnabled = params.isEditingNameEnabled,
onEditNameClick = ::startEditingDisplayName,
onReveal = ::revealCardDetails,
onReveal = ::requestReveal,
onCopy = ::copyData,
)
@ -99,7 +99,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
) {
val card = status.requireCardWithId(cardId)
if (card.isReissuing) {
hideCardDetails()
requestHide()
}
card.displayName?.let { uiState.update(TangemPayCardDetailsUpdateNameTransformer(it)) }
uiState.update { uiState ->
@ -124,18 +124,26 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
.launchIn(modelScope)
}
private fun requestReveal() {
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Show) }
}
private fun requestHide() {
modelScope.launch { cardDetailsEventListener.send(CardDetailsEvent.Hide) }
}
private fun revealCardDetails() {
analytics.send(TangemPayAnalyticsEvents.ViewCardDetailsClicked())
modelScope.launch {
uiState.transformerUpdate(
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::requestHide),
)
cardDetailsRepository.revealCardDetails(params.userWalletId)
.onRight { cardDetails ->
uiState.transformerUpdate(
transformer = DetailsRevealedStateTransformer(
details = cardDetails,
onClickHide = ::hideCardDetails,
onClickHide = ::requestHide,
),
)
launchShowDetailsTimer()
@ -150,11 +158,11 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private fun launchShowDetailsTimer() {
modelScope.launch {
delay(SHOW_DETAILS_TIME)
hideCardDetails()
requestHide()
}.saveIn(showCardDetailsTimerJobHolder)
}
fun hideCardDetails() {
private fun hideCardDetails() {
revealCardDetailsJobHolder.cancel()
uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory))
}

View file

@ -12,6 +12,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.format.bigdecimal.fiat
@ -39,6 +40,8 @@ import com.tangem.features.tangempay.components.TangemPayCardPageComponent
import com.tangem.features.tangempay.components.ViewPinListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.*
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
import com.tangem.features.tangempay.utils.cryptoCurrency
@ -49,9 +52,11 @@ import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.tangem.core.ui.R as CoreUiR
@Suppress("LongParameterList")
@Stable
@ -65,6 +70,8 @@ internal class TangemPayCardPageModel @Inject constructor(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val uiMessageSender: UiMessageSender,
private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase,
private val designFeatureToggles: DesignFeatureToggles,
private val cardDetailsEventListener: CardDetailsEventListener,
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener {
private val params: TangemPayCardPageComponent.Params = paramsContainer.require()
@ -82,6 +89,7 @@ internal class TangemPayCardPageModel @Inject constructor(
onBackClick = router::pop,
dailyLimitState = TangemPayDailyLimitBlockState.Loading,
settings = persistentListOf(),
settingsV2 = persistentListOf(),
),
)
@ -90,6 +98,7 @@ internal class TangemPayCardPageModel @Inject constructor(
init {
analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened())
fetchAddToWalletBanner()
modelScope.launch { subscribeOnDetailsState() }
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->
@ -115,6 +124,7 @@ internal class TangemPayCardPageModel @Inject constructor(
uiState.copy(
dailyLimitState = dailyLimitState,
settings = buildSettings(card),
settingsV2 = buildSettingsV2(card),
isReissueInProgress = card.isReissuing,
)
}
@ -126,6 +136,7 @@ internal class TangemPayCardPageModel @Inject constructor(
}
private fun buildSettings(card: TangemPayCard): ImmutableList<TangemPayCardPageSetting> {
if (designFeatureToggles.isRedesignEnabled) return persistentListOf()
return persistentListOf(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
@ -150,6 +161,64 @@ internal class TangemPayCardPageModel @Inject constructor(
)
}
private suspend fun subscribeOnDetailsState() {
if (!designFeatureToggles.isRedesignEnabled) return
cardDetailsEventListener.event.collect { event ->
val isDetailsShown = event == CardDetailsEvent.Show
uiState.update { state ->
state.copy(
settingsV2 = state.settingsV2
.map { setting ->
if (setting.id == TangemPayCardPageSettingV2.Id.Details) {
setting.copy(isEnabled = !isDetailsShown)
} else {
setting
}
}
.toImmutableList(),
)
}
}
}
private fun buildSettingsV2(card: TangemPayCard): ImmutableList<TangemPayCardPageSettingV2> {
if (!designFeatureToggles.isRedesignEnabled) return persistentListOf()
return persistentListOf(
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.Details,
title = TextReference.Res(R.string.details_title),
onClick = ::onClickViewDetails,
iconRes = CoreUiR.drawable.ic_visa_card_details_24,
),
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.Freeze,
title = TextReference.Res(
if (card.isFrozen) {
R.string.tangem_pay_freeze_card_unfreeze
} else {
R.string.tangem_pay_freeze_card_freeze
},
),
onClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) },
iconRes = CoreUiR.drawable.ic_freeze_24,
testTag = TangemPayTestTags.FREEZE_CARD_ROW,
),
TangemPayCardPageSettingV2(
id = TangemPayCardPageSettingV2.Id.ChangePin,
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onClick = { onClickChangePIN(card.hasPinCode) },
iconRes = CoreUiR.drawable.ic_card_pin_24,
testTag = TangemPayTestTags.CHANGE_PIN_ROW,
),
)
}
private fun onClickViewDetails() {
modelScope.launch(dispatchers.default) {
cardDetailsEventListener.send(CardDetailsEvent.Show)
}
}
private fun onClickLimitChange() {
analytics.send(TangemPayAnalyticsEvents.LimitChangeClicked())
router.push(TangemPayCardDetailsInnerRoute.LimitSetup)
@ -284,6 +353,7 @@ internal class TangemPayCardPageModel @Inject constructor(
addToWalletBlockState = AddToWalletBlockState(
onClick = ::onClickAddToWallet,
onClickClose = ::onClickCloseBanner,
shouldUseMagicEffect = false,
),
)
}

View file

@ -15,7 +15,11 @@ internal class DetailsAddToWalletBannerTransformer(
addToWalletBlockState = if (isDone) {
null
} else {
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
AddToWalletBlockState(
onClick = onClickBanner,
onClickClose = onClickCloseBanner,
shouldUseMagicEffect = true,
)
},
)
}

View file

@ -18,7 +18,12 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
@ -32,6 +37,15 @@ private const val GRADIENT_RADIUS = 200F
@Composable
internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
TangemPayAddToWalletBlockV2(state = state, modifier = modifier)
} else {
TangemPayAddToWalletBlockV1(state = state, modifier = modifier)
}
}
@Composable
internal fun TangemPayAddToWalletBlockV1(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
@ -55,7 +69,9 @@ internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: M
Image(
painter = painterResource(R.drawable.img_google_wallet_48),
contentDescription = null,
modifier = Modifier.size(36.dp).clip(CircleShape),
modifier = Modifier
.size(36.dp)
.clip(CircleShape),
)
Spacer(Modifier.width(12.dp))
@ -97,11 +113,26 @@ internal fun TangemPayAddToWalletBlock(state: AddToWalletBlockState, modifier: M
}
}
@Composable
internal fun TangemPayAddToWalletBlockV2(state: AddToWalletBlockState, modifier: Modifier = Modifier) {
TangemMessage(
modifier = modifier.clickableSingle(onClick = state.onClick),
onCloseClick = state.onClickClose,
title = resourceReference(R.string.tangempay_card_details_open_wallet_notification_title),
subtitle = resourceReference(R.string.tangempay_card_details_open_wallet_notification_subtitle),
messageEffect = if (state.shouldUseMagicEffect) {
TangemMessageEffect.Magic
} else {
TangemMessageEffect.None
},
)
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewTangemPayAddToWalletBlock() {
TangemThemePreview {
TangemPayAddToWalletBlock(AddToWalletBlockState({}, {}))
TangemPayAddToWalletBlock(AddToWalletBlockState(onClick = {}, onClickClose = {}, shouldUseMagicEffect = false))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@ -13,10 +14,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,9 +24,11 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
@ -37,18 +37,20 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.*
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.features.tangempay.details.impl.R
@ -129,23 +131,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
painter = painterResource(id = imageResId),
contentDescription = null,
)
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors.icon.constant,
contentDescription = null,
)
SpacerW4()
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
}
CardTopBlock()
if (state.isActionsAvailable) {
ConstraintLayout(
@ -167,18 +153,11 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
},
)
}
Text(
text = state.numberShort,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
modifier = Modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
CardNumberBlock(
numberShort = state.numberShort,
cardNumberRef = cardNumberRef,
)
when (state.cardFrozenState) {
TangemPayCardFrozenState.Frozen -> Icon(
modifier = Modifier
@ -209,23 +188,93 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
)
TangemPayCardFrozenState.Unfrozen -> Unit
}
TangemPayCardDetailsCustomButton(
AnimatedVisibility(
modifier = Modifier
.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
.testTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
)
visible = !LocalRedesignEnabled.current || state.isLoading,
) {
TangemPayCardDetailsCustomButton(
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
)
}
}
}
}
}
@Composable
private fun CardTopBlock(modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (LocalRedesignEnabled.current) {
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.primary,
)
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x5),
imageVector = ImageVector.vectorResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors3.icon.staticDark,
contentDescription = null,
)
} else {
Icon(
painter = painterResource(R.drawable.ic_cloud_fill_16),
tint = TangemTheme.colors.icon.constant,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.tangempay_digital_card),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
}
}
}
@Composable
private fun ConstraintLayoutScope.CardNumberBlock(
numberShort: String,
cardNumberRef: ConstrainedLayoutReference,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
Text(
text = numberShort,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.primary,
modifier = modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
)
} else {
Text(
text = numberShort,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
modifier = modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.padding(bottom = 8.dp),
)
}
}
@Composable
private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifier) {
when (state) {
@ -236,36 +285,67 @@ private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifi
@Composable
private fun DisplayOnlyCardDisplayName(state: DisplayNameState.Display, modifier: Modifier = Modifier) {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
painter = painterResource(id = R.drawable.ic_edit_new_12),
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = TangemTheme.colors.text.constantWhite,
if (LocalRedesignEnabled.current) {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.staticDark.secondary,
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_edit_card_20),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x5),
tint = TangemTheme.colors3.icon.staticDark,
)
}
}
} else {
Row(
modifier = modifier.conditional(
condition = state.isEditingEnabled,
modifier = { clickable(onClick = state.onClick) },
),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.displayName,
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
maxLines = 1,
)
if (state.isEditingEnabled) {
Icon(
painter = painterResource(id = R.drawable.ic_edit_new_12),
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = TangemTheme.colors.text.constantWhite,
)
}
}
}
}
@Composable
private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Modifier = Modifier) {
val isRedesignEnabled = LocalRedesignEnabled.current
val focusRequester = remember { FocusRequester() }
val placeholder = stringResourceSafe(R.string.tangempay_card_edit_name_placeholder)
val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
val textStyle = if (isRedesignEnabled) {
TangemTheme.typography3.body.medium.copy(color = TangemTheme.colors3.text.staticDark.primary)
} else {
TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
}
val textMeasurer = rememberTextMeasurer()
val measuredText = state.editingValue.text.ifEmpty { placeholder }
val textWidthDp = with(LocalDensity.current) {
@ -280,7 +360,11 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
.focusRequester(focusRequester),
textStyle = textStyle,
singleLine = true,
cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite),
cursorBrush = if (isRedesignEnabled) {
SolidColor(TangemTheme.colors3.text.staticDark.primary)
} else {
SolidColor(TangemTheme.colors.text.constantWhite)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = if (state.isSubmitEnabled) {
@ -294,7 +378,13 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
if (state.editingValue.text.isEmpty()) {
Text(
text = placeholder,
style = textStyle.copy(color = TangemTheme.colors.text.tertiary),
style = textStyle.copy(
color = if (isRedesignEnabled) {
TangemTheme.colors3.text.staticDark.secondary
} else {
TangemTheme.colors.text.tertiary
},
),
)
}
innerTextField()
@ -317,10 +407,7 @@ private fun TangemPayCardDetailsShownBlock(
onHideDetails: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize(),
) {
Column(modifier = modifier.fillMaxSize()) {
CardDetailsTextContainer(
modifier = Modifier
.padding(top = 16.dp, bottom = 12.dp, start = 16.dp, end = 16.dp)
@ -361,14 +448,31 @@ private fun TangemPayCardDetailsShownBlock(
Spacer(modifier = Modifier.weight(1f))
Row {
SpacerWMax()
TangemPayCardDetailsCustomButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_hide_details),
onClick = onHideDetails,
showProgress = false,
)
if (LocalRedesignEnabled.current) {
// Must use dark theme locally for button cause card is dark
CompositionLocalProvider(LocalIsInDarkTheme provides true) {
TangemThemeRedesign {
TangemButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
variant = TangemButton.Variant.Material,
size = TangemButton.Size.X8,
onClick = onHideDetails,
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_20),
)
}
}
} else {
TangemPayCardDetailsCustomButton(
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_hide_details),
onClick = onHideDetails,
showProgress = false,
)
}
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -26,8 +27,10 @@ import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
@ -44,6 +47,7 @@ internal fun TangemPayCardPageScreen(
cardDetailsState: TangemPayCardDetailsUM,
modifier: Modifier = Modifier,
) {
val isRedesignEnabled = LocalRedesignEnabled.current
Scaffold(
modifier = modifier,
topBar = {
@ -53,7 +57,11 @@ internal fun TangemPayCardPageScreen(
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
containerColor = if (isRedesignEnabled) {
TangemTheme.colors3.bg.primary
} else {
TangemTheme.colors.background.secondary
},
) { scaffoldPaddings ->
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
@ -73,6 +81,14 @@ internal fun TangemPayCardPageScreen(
state = cardDetailsState,
)
}
if (isRedesignEnabled && state.settingsV2.isNotEmpty()) {
cardPageItem("Settings buttons") {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier.fillMaxWidth(),
settings = state.settingsV2,
)
}
}
if (state.isReissueInProgress) {
cardPageItem(key = "Reissue") {
TangemPayReplacingCardBlock()
@ -104,6 +120,7 @@ private fun TangemPayCardPageSettingsBlock(
settings: ImmutableList<TangemPayCardPageSetting>,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) return
Column(
modifier = modifier
.fillMaxWidth()
@ -176,11 +193,27 @@ private fun LazyListScope.cardPageItem(
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun preview() = TangemThemePreview {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
private fun TangemPayCardPageScreenPreviewV1() {
TangemThemePreview {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
cvv = "•••",
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
),
),
),
cardDetailsState = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
@ -191,23 +224,52 @@ private fun preview() = TangemThemePreview {
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
isEditingEnabled = false,
),
),
),
cardDetailsState = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
cvv = "•••",
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = false,
),
),
)
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCardPageScreenPreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent(
TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
cvv = "•••",
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = true,
),
),
),
cardDetailsState = TangemPayCardDetailsUM(
number = "•••• •••• •••• 1245",
numberShort = "··1245",
expiry = "••/••",
cvv = "•••",
onCopy = { _, _ -> },
onClick = {},
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
displayNameState = DisplayNameState.Display(
displayName = "Tangem Pay Card",
onClick = {},
isEditingEnabled = false,
),
),
)
}
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.entity.TangemPayCardPageSettingV2
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun TangemPayCardPageSettingsButtonsBlock(
settings: ImmutableList<TangemPayCardPageSettingV2>,
modifier: Modifier = Modifier,
) {
if (settings.isEmpty()) return
Row(
modifier = modifier.padding(vertical = TangemTheme.dimens2.x6),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
settings.fastForEach { setting ->
TangemPaySettingButton(
setting = setting,
modifier = Modifier.then(if (setting.testTag != null) Modifier.testTag(setting.testTag) else Modifier),
)
}
}
}
@Composable
private fun TangemPaySettingButton(setting: TangemPayCardPageSettingV2, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(horizontal = TangemTheme.dimens2.x6),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemButton(
variant = TangemButton.Variant.Material,
size = TangemButton.Size.X14,
onClick = setting.onClick,
iconStart = TangemIconUM.Icon(
iconRes = setting.iconRes,
tintReference = { TangemTheme.colors3.icon.primary },
),
isLoading = setting.isLoading,
isEnabled = setting.isEnabled,
)
Text(
text = setting.title.resolveAnnotatedReference(),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayCardPageSettingsButtonsBlockPreview() {
TangemThemePreviewRedesign {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
settings = TangemPayCardPageSettingV2.stubList(),
)
}
}
@Preview(showBackground = true, name = "Frozen card")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Frozen card — dark")
@Composable
private fun TangemPayCardPageSettingsButtonsBlockFrozenPreview() {
TangemThemePreviewRedesign {
TangemPayCardPageSettingsButtonsBlock(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
settings = TangemPayCardPageSettingV2.stubList(isFrozen = true),
)
}
}

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.Text
@ -11,7 +12,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -19,15 +24,31 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
@Composable
internal fun TangemPayDailyLimitBlock(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
CurrentLimitBlockV2(state, modifier)
} else {
TangemPayDailyLimitBlockV1(state, modifier)
}
}
@Composable
private fun TangemPayDailyLimitBlockV1(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
@ -43,12 +64,12 @@ internal fun TangemPayDailyLimitBlock(state: TangemPayDailyLimitBlockState, modi
color = TangemTheme.colors.text.tertiary,
)
SpacerH4()
CurrentLimitBlock(state)
CurrentLimitBlockV1(state)
}
}
@Composable
private fun CurrentLimitBlock(state: TangemPayDailyLimitBlockState) {
private fun CurrentLimitBlockV1(state: TangemPayDailyLimitBlockState) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@ -107,8 +128,147 @@ private fun CurrentLimitBlock(state: TangemPayDailyLimitBlockState) {
}
}
@Composable
private fun CurrentLimitBlockV2(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier
.clip(RoundedCornerShape(TangemTheme.dimens2.x6))
.background(color = TangemTheme.colors3.bg.secondary),
contentPadding = PaddingValues(TangemTheme.dimens2.x4),
) {
LimitHeadIcon(
modifier = Modifier.layoutId(TangemRowLayoutId.HEAD),
state = state,
)
TitleLimit(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.START_TOP),
state = state,
)
SubtitleLimit(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.START_BOTTOM),
state = state,
)
if (state is TangemPayDailyLimitBlockState.Content) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
TangemButton(
modifier = Modifier
.padding(start = TangemTheme.dimens2.x3)
.layoutId(TangemRowLayoutId.TAIL),
variant = TangemButton.Variant.Secondary,
text = resourceReference(R.string.common_edit),
onClick = state.onChangeClick,
size = TangemButton.Size.X10,
)
}
}
}
}
@Composable
private fun LimitHeadIcon(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x10)
.background(
color = when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> TangemTheme.colors3.bg.status.infoSubtle
TangemPayDailyLimitBlockState.Error -> TangemTheme.colors3.bg.status.warningSubtle
},
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_limit_new_20),
contentDescription = null,
tint = TangemTheme.colors3.icon.brand,
)
}
TangemPayDailyLimitBlockState.Error -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_warning_20),
contentDescription = null,
tint = TangemTheme.colors3.icon.status.warning,
)
}
}
}
}
@Composable
private fun TitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayDailyLimitBlockState.Content,
TangemPayDailyLimitBlockState.Loading,
-> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_title),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
)
}
TangemPayDailyLimitBlockState.Error -> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_error_title),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
}
}
}
@Composable
private fun SubtitleLimit(state: TangemPayDailyLimitBlockState, modifier: Modifier = Modifier) {
when (state) {
TangemPayDailyLimitBlockState.Error -> {
Text(
modifier = modifier,
text = stringResourceSafe(R.string.tangempay_card_page_daily_limit_error_subtitle),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
is TangemPayDailyLimitBlockState.Content -> {
Text(
modifier = modifier,
text = state.limit,
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
TangemPayDailyLimitBlockState.Loading -> {
TextShimmer(
radius = TangemTheme.dimens2.x25,
modifier = modifier,
style = TextShimmerStyle.BODY,
text = "$50,000",
)
}
}
}
@Composable
internal fun TangemPayDailyLimitErrorBlock(modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) return
Notification(
config = NotificationConfig(
title = resourceReference(R.string.tangempay_card_page_daily_limit_error_title),
@ -123,13 +283,33 @@ internal fun TangemPayDailyLimitErrorBlock(modifier: Modifier = Modifier) {
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun preview() = TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
private fun Preview() {
TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewV2() {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Content.stub())
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Error)
TangemPayDailyLimitBlock(state = TangemPayDailyLimitBlockState.Loading)
TangemPayDailyLimitErrorBlock()
}
}
}
}

View file

@ -457,7 +457,11 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
),
isBalanceHidden = false,
addFundsEnabled = true,
addToWalletBlockState = AddToWalletBlockState(onClick = {}, onClickClose = {}),
addToWalletBlockState = AddToWalletBlockState(
onClick = {},
onClickClose = {},
shouldUseMagicEffect = false,
),
accountDeactivatedNotificationConfig = null,
),
TangemPayDetailsUM(