We have carried out the closure of the LINE LIVE app. Given that this app had a substantial user base, with many users potentially having made significant payments within the app, we couldn't abruptly remove the app from the store and cease user support.
Some of the challenges we needed to tackle included notifying users, offering compensation, and determining how to secure users' acceptance while preserving their trust in our company and group.
The plan
Step 1: Notification
To widely disseminate the company's decision to discontinue the LINE LIVE service, we needed to notify users through the app, on social media channels, and send information directly to users. Simultaneously, while notifying users, it was crucial to clearly explain the reasons behind discontinuing the service. This transparency would aid users in understanding and accepting our decision.
Step 2: Compensation
We needed to provide compensation to users who made purchases within the app. This could involve refunding their money or providing offers to compensate for their losses and any inconvenience caused.
Step 3: Support
We needed to continue supporting users for a certain period of time after notifying them about the service discontinuation. This approach would assist users in clarifying any questions they might have and would help maintain their trust in us.
Step 4: Remove
After completing the aforementioned steps, we would proceed with the final step of removing the app from the store and discontinuing support.
The execution
Backend
Whenever an API request is made from the web or app, we check the SERVICE_CLOSE_MODE
status and return it via a header to the App/Web along with the response.
App
API pre-processing
Before initiating API calls and awaiting responses from the server, our application requires some processing. To facilitate this, we are using OkHttp and have developed an ApiInterceptor, which we have subsequently incorporated into the OkHttpClient.
Whenever we receive a response from the backend, it's essential to verify the X-CastService-Meta-Type (referenced as [3] in the backend diagram) in the header. If the returned type equals SERVICE_CLOSE, we then proceed to process the data present in META_DATA. The data will adhere to the following format:
{
"coinRefundText":"未使用のLIVEコインの払戻し及び...", // A information for refund coin
"coinRefundUrl":"....",
"inquiryUrl":"https://...",
"serviceCloseText":"2023年3月31日を持ちましてサービス終了致しました。..."
}
To accomplish this, we have the capability to override the intercept function in OkHttp's Interceptor interface. This grants us the ability to execute the requisite actions as needed. To delve deeper, the intercept function in OkHttp's Interceptor interface is invoked for every request and response transmitted and received by the client.
By overriding this function, we can tailor the client's behavior to meet our requirements. Specifically, we are examining the `META_TYPE` in the response header and processing the `META_DATA` accordingly if the type is `SERVICE_CLOSE`.
internal class ApiInterceptor(
private val headerMetaMessageRepository: HeaderMetaMessageRepository?,
private val closeServiceRepository: CloseServiceRepository?,
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
try {
val builder = chain.request().newBuilder()
val response = interceptResponse(chain.proceed(builder.build()))
return response
} catch (e: NetworkException) {
closeServiceRepository?.notifyCloseService()
throw e
}
}
@Throws(IOException::class)
private fun interceptResponse(response: Response): Response {
....
response.header(HeaderName.META_TYPE)?.let { type ->
deCodingUTF8(response.header(HeaderName.META_DATA))?.let { data ->
processInterceptByHeaderMeta(type, data)
}
}
....
return response
}
private fun processInterceptByHeaderMeta(type: String, data: String) {
when (type) {
MetaType.SERVICE_CLOSE.name -> {
val closeServiceMessageData = getMetaData<CloseServiceMessageData>(data) // Parse meta data from json
val headerMetaMessage = HeaderMetaMessage(
announceMessage = CloseServiceMessageData.TAG,
type = MetaType.SERVICE_CLOSE,
messageData = closeServiceMessageData.let { messageData ->
Bundle().also {
it.putSerializable(CloseServiceMessageData.TAG, messageData)
}
}
)
headerMetaMessageRepository?.notifyReceived(headerMetaMessage)
}
else -> ...
}
}
}
To provide further insight into `CloseServiceRepository` and `HeaderMetaMessageRepository`, when we receive a `META_TYPE` of `CLOSE_SERVICE`, we utilize the `HeaderMetaMessageRepository` to dispatch a notification indicating that we have received a response with the `SERVICE_CLOSE` header.
However, in future scenarios, our backend may be entirely shut down. In such cases, processing the response will trigger a `NetworkException`.
In this eventuality, the `CloseServiceRepository` will be tasked with notifying an event, enabling us to verify if we have previously received a `SERVICE_CLOSE`. If we have, we will then process the information related to the most recent service closure we received.
In summary, `CloseServiceRepository` and `HeaderMetaMessageRepository` collaborate to manage responses with the `SERVICE_CLOSE` header.
`HeaderMetaMessageRepository` is employed to dispatch a notification when a `SERVICE_CLOSE` header is received, while `CloseServiceRepository` is charged with managing scenarios where the backend is entirely closed, and notifying an event to verify if we have previously received a `SERVICE_CLOSE`.
LineCastApp
Initially, establish and configure `HeaderMetaMessageRepository` and `CloseServiceRepository` for `ApiClientCreator`, which is utilized to create the OkHttpClient that initiates the API calls. Additionally, `openCloseServiceScreenEvent` is employed to allow activities to listen to the `ServiceClose` screen opening event.
class LineCastApp : Application(), HasAndroidInjector {
@Inject
lateinit var headerMetaMessageRepository: HeaderMetaMessageRepository
@Inject
lateinit var closeServiceRepository: CloseServiceRepository
private lateinit var configuredApiClientCreator: ApiClientCreator
private val openCloseServiceScreenEvent = MutableSharedFlow<CloseServiceMessageData?>()
...
configuredApiClientCreator = ApiClientCreator(userAgent)
.context(this)
.messageResponseRepository(headerMetaMessageRepository)
.closeServiceRepository(closeServiceRepository)
...
fun openCloseServiceScreenEvent() = getInstance().openCloseServiceScreenEvent.asSharedFlow()
Following the setup, we need to monitor both `HeaderMetaMessageRepository` and `CloseServiceRepository` when receiving events from them. With `HeaderMetaMessageRepository`, each time we receive an event with the response type `SERVICE_CLOSE`,
We will preserve the state in `SharedPreference` and emit an event for `openCloseServiceScreenEvent`. Regarding `CloseServiceRepository`, each time we receive an event, we need to verify if there is any `SERVICE_CLOSE` data saved in `SharedPreference`.
If such data exists, we emit an event for `openCloseServiceScreenEvent`. If not, we disregard the event.
headerMetaMessageRepository.messageResponseObservable // Notified from headerMetaMessageRepository#notifyReceived
....
.subscribe { response: HeaderMetaMessage ->
if (response.type == MetaType.SERVICE_CLOSE) {(
response.messageData?.getSerializable(
CloseServiceMessageData.TAG
) as? CloseServiceMessageData
).let { data ->
getInstance().forceOpenCloseServiceIfNeed(data)
}
}
...
}
closeServiceRepository.closeServiceObservable // Notified from closeServiceRepository#notifyCloseService
....
.subscribe {
forceOpenCloseServiceIfNeed()
}
private fun forceOpenCloseServiceIfNeed(
closeServiceMessageData: CloseServiceMessageData? = null
) {
val data = closeServiceMessageData ?: ...
data?.let {
coroutineScope.launch {
openCloseServiceScreenEvent.emit(it)
}
}
}
Activities
Subsequently, we need to monitor the `openCloseServiceScreenEvent` in all Activities. To accomplish this, we need to create a `CloseServiceLifeCycleEvent` that implements `DefaultLifecycleObserver`.
In this class, we override the `onStart` method, and all Activities will observe `closeServiceLifeCycleEvent` or check `closeServiceData` in `SharedPreference` and open the `ServiceClose` Screen if required.
In summary, we are developing a `CloseServiceLifeCycleEvent` to observe the `openCloseServiceScreenEvent` in all Activities.
By implementing `DefaultLifecycleObserver` and overriding the `onStart` method, we can ensure that all Activities will open the `ServiceClose` Screen when necessary.
class CloseServiceLifeCycleEvent(private val activity: Activity) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
// Always open close service screen if saved in local
// getPreferenceUtils() is support set and get SharedPreference
LineCastApp.getPreferenceUtils().closeServiceData?.let {
openCloseServiceActivity(it)
}
owner.repeatWhenStarted {
launch {
LineCastApp.openCloseServiceScreenEvent().collectLatest {
openCloseServiceActivity(it)
}
}
}
}
}
// Activity
class ExampleActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(CloseServiceLifeCycleEvent(this))
}
}
Finally, when launching the `CloseService` screen, we will showcase the user interface along with the associated data:
Users can click on `Request a refund` to open a web page and initiate a refund request.
That concludes all the necessary steps to close the LINE LIVE app.
We hope this article offers you a thorough understanding of the process involved in removing an app, particularly LINE LIVE.