diff --git a/app/src/googleplay/java/org/tasks/analytics/Firebase.kt b/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
index 92522fbc6..26c0bf9d9 100644
--- a/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
+++ b/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
@@ -22,7 +22,6 @@ class Firebase @Inject constructor(
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
-
private var crashlytics: FirebaseCrashlytics? = null
private var analytics: FirebaseAnalytics? = null
private var remoteConfig: FirebaseRemoteConfig? = null
@@ -76,11 +75,8 @@ class Firebase @Inject constructor(
get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
- val moreOptionsBadge: Boolean
- get() = remoteConfig?.getBoolean("more_options_badge") ?: false
-
- val moreOptionsSolid: Boolean
- get() = remoteConfig?.getBoolean("more_options_solid") ?: false
+ val subGroupA: Boolean
+ get() = remoteConfig?.getBoolean("sub_group_a") ?: false
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1517b3755..5842d8a6d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -302,7 +302,7 @@
+ android:theme="@style/Tasks" />
diff --git a/app/src/main/java/org/tasks/billing/PurchaseActivity.kt b/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
index 2f9a7bb7a..b96102df2 100644
--- a/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
+++ b/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
@@ -4,17 +4,18 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.analytics.Firebase
-import org.tasks.compose.PurchaseText.PurchaseText
+import org.tasks.compose.PurchaseText.SubscriptionScreen
import org.tasks.extensions.Context.toast
import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme
@@ -56,16 +57,19 @@ class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
setContent {
TasksTheme {
- Dialog(onDismissRequest = { finish() }) {
- PurchaseText(
- nameYourPrice = nameYourPrice,
- sliderPosition = sliderPosition,
- github = github,
- solidButton = firebase.moreOptionsSolid,
- badge = firebase.moreOptionsBadge,
- onDisplayed = { firebase.logEvent(R.string.event_showed_purchase_dialog) },
- subscribe = this::purchase,
- )
+ BackHandler {
+ finish()
+ }
+ SubscriptionScreen(
+ nameYourPrice = nameYourPrice,
+ sliderPosition = sliderPosition,
+ github = github,
+ hideText = firebase.subGroupA,
+ subscribe = this::purchase,
+ onBack = { finish() },
+ )
+ LaunchedEffect(key1 = Unit) {
+ firebase.logEvent(R.string.event_showed_purchase_dialog)
}
}
}
diff --git a/app/src/main/java/org/tasks/compose/Pager.kt b/app/src/main/java/org/tasks/compose/Pager.kt
deleted file mode 100644
index b41b2fe77..000000000
--- a/app/src/main/java/org/tasks/compose/Pager.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright 2020 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
- *
- * https://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.
- */
-
-package org.tasks.compose
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.gestures.rememberDraggableState
-import androidx.compose.foundation.layout.Box
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.ParentDataModifier
-import androidx.compose.ui.unit.Density
-import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
-
-/**
- * This is a modified version of:
- * https://gist.github.com/adamp/07d468f4bcfe632670f305ce3734f511
- */
-
-class PagerState(
- currentPage: Int = 0,
- minPage: Int = 0,
- maxPage: Int = 0
-) {
- private var _minPage by mutableStateOf(minPage)
- var minPage: Int
- get() = _minPage
- set(value) {
- _minPage = value.coerceAtMost(_maxPage)
- _currentPage = _currentPage.coerceIn(_minPage, _maxPage)
- }
-
- private var _maxPage by mutableStateOf(maxPage, structuralEqualityPolicy())
- var maxPage: Int
- get() = _maxPage
- set(value) {
- _maxPage = value.coerceAtLeast(_minPage)
- _currentPage = _currentPage.coerceIn(_minPage, maxPage)
- }
-
- private var _currentPage by mutableStateOf(currentPage.coerceIn(minPage, maxPage))
- var currentPage: Int
- get() = _currentPage
- set(value) {
- _currentPage = value.coerceIn(minPage, maxPage)
- }
-
- enum class SelectionState { Selected, Undecided }
-
- var selectionState by mutableStateOf(SelectionState.Selected)
-
- private suspend fun selectPage() {
- currentPage -= currentPageOffset.roundToInt()
- snapToOffset(0f)
- selectionState = SelectionState.Selected
- }
-
- private var _currentPageOffset = Animatable(0f).apply {
- updateBounds(-1f, 1f)
- }
- val currentPageOffset: Float
- get() = _currentPageOffset.value
-
- suspend fun snapToOffset(offset: Float) {
- val max = if (currentPage == minPage) 0f else 1f
- val min = if (currentPage == maxPage) 0f else -1f
- _currentPageOffset.snapTo(offset.coerceIn(min, max))
- }
-
- suspend fun fling(velocity: Float) {
- if (velocity < 0 && currentPage == maxPage) return
- if (velocity > 0 && currentPage == minPage) return
-
- _currentPageOffset.animateTo(currentPageOffset.roundToInt().toFloat())
- selectPage()
- }
-
- override fun toString(): String = "PagerState{minPage=$minPage, maxPage=$maxPage, " +
- "currentPage=$currentPage, currentPageOffset=$currentPageOffset}"
-}
-
-@Immutable
-private data class PageData(val page: Int) : ParentDataModifier {
- override fun Density.modifyParentData(parentData: Any?): Any = this@PageData
-}
-
-private val Measurable.page: Int
- get() = (parentData as? PageData)?.page ?: error("no PageData for measurable $this")
-
-@Composable
-fun Pager(
- state: PagerState,
- modifier: Modifier = Modifier,
- offscreenLimit: Int = 2,
- pageContent: @Composable PagerScope.() -> Unit
-) {
- var pageSize by remember { mutableStateOf(0) }
- val coroutineScope = rememberCoroutineScope()
- Layout(
- content = {
- val minPage = (state.currentPage - offscreenLimit).coerceAtLeast(state.minPage)
- val maxPage = (state.currentPage + offscreenLimit).coerceAtMost(state.maxPage)
-
- for (page in minPage..maxPage) {
- val pageData = PageData(page)
- val scope = PagerScope(page)
- key(pageData) {
- Box(contentAlignment = Alignment.Center, modifier = pageData) {
- scope.pageContent()
- }
- }
- }
- },
- modifier = modifier.draggable(
- orientation = Orientation.Horizontal,
- onDragStarted = {
- state.selectionState = PagerState.SelectionState.Undecided
- },
- onDragStopped = { velocity ->
- coroutineScope.launch {
- // Velocity is in pixels per second, but we deal in percentage offsets, so we
- // need to scale the velocity to match
- state.fling(velocity / pageSize)
- }
- },
- state = rememberDraggableState { dy ->
- coroutineScope.launch {
- with(state) {
- val pos = pageSize * currentPageOffset
- val max = if (currentPage == minPage) 0 else pageSize * offscreenLimit
- val min = if (currentPage == maxPage) 0 else -pageSize * offscreenLimit
- val newPos = (pos + dy).coerceIn(min.toFloat(), max.toFloat())
- snapToOffset(newPos / pageSize)
- }
- }
- },
- )
- ) { measurables, constraints ->
- layout(constraints.maxWidth, constraints.maxHeight) {
- val currentPage = state.currentPage
- val offset = state.currentPageOffset
- val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
-
- measurables
- .map {
- it.measure(childConstraints) to it.page
- }
- .forEach { (placeable, page) ->
- // TODO: current this centers each page. We should investigate reading
- // gravity modifiers on the child, or maybe as a param to Pager.
- val xCenterOffset = (constraints.maxWidth - placeable.width) / 2
- val yCenterOffset = (constraints.maxHeight - placeable.height) / 2
-
- if (currentPage == page) {
- pageSize = placeable.width
- }
-
- val xItemOffset = ((page + offset - currentPage) * placeable.width).roundToInt()
-
- placeable.place(
- x = xCenterOffset + xItemOffset,
- y = yCenterOffset
- )
- }
- }
- }
-}
-
-/**
- * Scope for [Pager] content.
- */
-class PagerScope(
- val page: Int
-)
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/compose/Subscription.kt b/app/src/main/java/org/tasks/compose/Subscription.kt
index 1811cea70..cc4f1cf3f 100644
--- a/app/src/main/java/org/tasks/compose/Subscription.kt
+++ b/app/src/main/java/org/tasks/compose/Subscription.kt
@@ -8,31 +8,43 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.requiredSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Badge
-import androidx.compose.material3.BadgedBox
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
@@ -45,10 +57,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.compose.Constants.HALF_KEYLINE
import org.tasks.compose.Constants.KEYLINE_FIRST
-import org.tasks.compose.PurchaseText.PurchaseText
+import org.tasks.compose.PurchaseText.SubscriptionScreen
import org.tasks.extensions.Context.openUri
import org.tasks.themes.TasksTheme
@@ -59,7 +72,7 @@ object PurchaseText {
val title: Int,
val icon: Int,
val description: Int,
- val tint: Boolean = true
+ val tint: Boolean = true,
)
private val featureList = listOf(
@@ -121,53 +134,93 @@ object PurchaseText {
)
)
+ @OptIn(ExperimentalMaterial3Api::class)
@Composable
- fun PurchaseText(
+ fun SubscriptionScreen(
nameYourPrice: MutableState = mutableStateOf(false),
sliderPosition: MutableState = mutableStateOf(0f),
github: Boolean = false,
- solidButton: Boolean = false,
- badge: Boolean = false,
- onDisplayed: () -> Unit = {},
- subscribe: (Int, Boolean) -> Unit
+ hideText: Boolean,
+ subscribe: (Int, Boolean) -> Unit,
+ onBack: () -> Unit,
) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .verticalScroll(rememberScrollState())
- .background(color = colorResource(R.color.content_background)),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- GreetingText(R.string.upgrade_blurb_1)
- GreetingText(R.string.upgrade_blurb_2)
- Spacer(Modifier.height(KEYLINE_FIRST))
- val pagerState = remember {
- PagerState(maxPage = (featureList.size - 1).coerceAtLeast(0))
- }
- Pager(
- state = pagerState,
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = stringResource(R.string.upgrade_to_pro),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.background
+ )
+ )
+ },
+ ) { paddingValues ->
+ Column(
modifier = Modifier
- .fillMaxWidth()
- .height(200.dp)
+ .padding(paddingValues)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .background(color = colorResource(R.color.content_background)),
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
- PagerItem(featureList[page], nameYourPrice.value && page == 0)
- }
- if (github) {
- SponsorButton()
- } else {
- GooglePlayButtons(
- nameYourPrice = nameYourPrice,
- sliderPosition = sliderPosition,
- pagerState = pagerState,
- subscribe = subscribe,
- solidButton = solidButton,
- badge = badge,
- )
+ if (!hideText) {
+ GreetingText(R.string.upgrade_blurb_1)
+ GreetingText(R.string.upgrade_blurb_2)
+ }
+ Spacer(Modifier.height(KEYLINE_FIRST))
+ val pagerState = rememberPagerState {
+ featureList.size
+ }
+ HorizontalPager(
+ state = pagerState // Optional: to control the pager's state
+ ) { index ->
+ val item = featureList[index]
+ PagerItem(item, nameYourPrice.value && index == 0)
+ }
+ Row(
+ Modifier
+ .wrapContentHeight()
+ .fillMaxWidth()
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 8.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ repeat(pagerState.pageCount) { iteration ->
+ val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
+ Box(
+ modifier = Modifier
+ .padding(2.dp)
+ .clip(CircleShape)
+ .background(color)
+ .size(16.dp)
+ )
+ }
+ }
+ if (github) {
+ SponsorButton()
+ } else {
+ GooglePlayButtons(
+ nameYourPrice = nameYourPrice,
+ sliderPosition = sliderPosition,
+ pagerState = pagerState,
+ subscribe = subscribe,
+ )
+ }
}
}
- LaunchedEffect(key1 = Unit) {
- onDisplayed()
- }
}
@Composable
@@ -212,47 +265,35 @@ object PurchaseText {
sliderPosition: MutableState,
pagerState: PagerState,
subscribe: (Int, Boolean) -> Unit,
- solidButton: Boolean,
- badge: Boolean,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- Divider(color = MaterialTheme.colorScheme.onSurface, thickness = 0.25.dp)
- Spacer(Modifier.height(KEYLINE_FIRST))
+ HorizontalDivider(modifier = Modifier.padding(vertical = KEYLINE_FIRST))
if (nameYourPrice.value) {
NameYourPrice(sliderPosition, subscribe)
} else {
TasksAccount(subscribe)
}
Spacer(Modifier.height(KEYLINE_FIRST))
+ val scope = rememberCoroutineScope()
OutlinedButton(
onClick = {
nameYourPrice.value = !nameYourPrice.value
- pagerState.currentPage = 0
+ scope.launch {
+ pagerState.animateScrollToPage(0)
+ }
},
colors = ButtonDefaults.textButtonColors(
- containerColor = if (solidButton)
- MaterialTheme.colorScheme.primary
- else
- Color.Transparent
+ containerColor = Color.Transparent
)
) {
- BadgedBox(badge = {
- if (!nameYourPrice.value && badge) {
- Badge()
- }
- }) {
- Text(
- text = stringResource(R.string.more_options),
- color = if (solidButton)
- MaterialTheme.colorScheme.onSecondary
- else
- MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyLarge
- )
- }
+ Text(
+ text = stringResource(R.string.more_options),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge
+ )
}
Text(
text = stringResource(R.string.pro_free_trial),
@@ -269,56 +310,54 @@ object PurchaseText {
@Composable
fun PagerItem(
feature: CarouselItem,
- disabled: Boolean = false
+ disabled: Boolean = false,
) {
- Column {
- Box(
+ Box(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(
modifier = Modifier
- .weight(1f)
- .fillMaxWidth(.5f),
+ .width(250.dp)
+ .height(150.dp)
+ .padding(HALF_KEYLINE),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
- Column(
+ Image(
+ painter = painterResource(feature.icon),
+ contentDescription = null,
+ modifier = Modifier.requiredSize(72.dp),
+ alignment = Alignment.Center,
+ colorFilter = if (feature.tint) {
+ ColorFilter.tint(colorResource(R.color.icon_tint_with_alpha))
+ } else {
+ null
+ }
+ )
+ Text(
+ text = stringResource(feature.title),
modifier = Modifier
.fillMaxWidth()
- .padding(HALF_KEYLINE),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Image(
- painter = painterResource(feature.icon),
- contentDescription = null,
- modifier = Modifier.requiredSize(72.dp),
- alignment = Alignment.Center,
- colorFilter = if (feature.tint) {
- ColorFilter.tint(colorResource(R.color.icon_tint_with_alpha))
- } else {
- null
- }
- )
- Text(
- text = stringResource(feature.title),
- modifier = Modifier
- .fillMaxWidth()
- .padding(0.dp, 4.dp),
- color = MaterialTheme.colorScheme.onBackground,
- style = TextStyle(
- fontWeight = FontWeight.Bold,
- fontSize = 14.sp,
- letterSpacing = 0.25.sp
- ),
- textAlign = TextAlign.Center
- )
- Text(
- text = stringResource(if (disabled) R.string.account_not_included else feature.description),
- modifier = Modifier.fillMaxWidth(),
- color = if (disabled) Color.Red else MaterialTheme.colorScheme.onBackground,
- style = TextStyle(
- fontWeight = if (disabled) FontWeight.Bold else FontWeight.Normal,
- fontSize = 12.sp,
- letterSpacing = 0.4.sp
- ),
- textAlign = TextAlign.Center,
- )
- }
+ .padding(0.dp, 4.dp),
+ color = MaterialTheme.colorScheme.onBackground,
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ letterSpacing = 0.25.sp
+ ),
+ textAlign = TextAlign.Center
+ )
+ Text(
+ text = stringResource(if (disabled) R.string.account_not_included else feature.description),
+ modifier = Modifier.fillMaxWidth(),
+ color = if (disabled) Color.Red else MaterialTheme.colorScheme.onBackground,
+ style = TextStyle(
+ fontWeight = if (disabled) FontWeight.Bold else FontWeight.Normal,
+ fontSize = 12.sp,
+ letterSpacing = 0.4.sp
+ ),
+ textAlign = TextAlign.Center,
+ )
}
}
}
@@ -355,7 +394,7 @@ object PurchaseText {
price: Int,
monthly: Boolean = false,
popperText: String = "",
- onClick: (Int, Boolean) -> Unit
+ onClick: (Int, Boolean) -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(
@@ -434,24 +473,23 @@ object PurchaseText {
@Composable
private fun PurchaseDialogPreview() {
TasksTheme {
- PurchaseText { _, _ -> }
- }
-}
-
-@Preview(showBackground = true)
-@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
-@Composable
-private fun PurchaseDialogPreviewSolid() {
- TasksTheme {
- PurchaseText(solidButton = true) { _, _ -> }
+ SubscriptionScreen(
+ hideText = false,
+ subscribe = { _, _ -> },
+ onBack = {},
+ )
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
-private fun PurchaseDialogPreviewBadge() {
+private fun PurchaseDialogPreviewNoText() {
TasksTheme {
- PurchaseText(badge = true) { _, _ -> }
+ SubscriptionScreen(
+ hideText = true,
+ subscribe = { _, _ -> },
+ onBack = {},
+ )
}
}