Panagiotis Triantafyllou

get coupons request, single coupon activity

Showing 25 changed files with 1619 additions and 481 deletions
......@@ -18,11 +18,6 @@ Debug=true
# DEH Development: https://engage-uat.dei.gr
BaseURL=https://engage-uat.dei.gr
# For Verify Ticket request
VerifyURL=/partners/dei/verify
#WebActionHandler=app_package_name.WarplyWebActionHandler
# Replace the color with one you want the progress bar to have depending on you app theme-coloring
# If not defined the colorPrimary will used
#ProgressColor=red
......@@ -46,13 +41,10 @@ SendPackages=false
# The app language
Language=el
# The merchant id for some requests
MerchantId=20113
# The login type must be one of the below:
# email, msisdn, username
LoginType=username
LoginType=email
# The deeplink url scheme for react native campaigns:
# Example visit.greece.gr
DL_URL_SCHEME=demo
\ No newline at end of file
# Example demo.app.gr
DL_URL_SCHEME=demo.app.gr
\ No newline at end of file
......
#Fri Jul 26 17:08:44 EEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
......
# Warply Android SDK — AI Agent Skill File
> **Purpose**: This file provides comprehensive context about the Warply Android SDK codebase for AI coding agents (Claude Code, Gemini CLI, Codex, OpenCode, Cline, etc.). It describes architecture, patterns, conventions, and critical rules to follow when working with this project.
---
## 1. Project Overview
- **What**: Warply Android SDK — a loyalty, marketing, and engagement SDK for Android apps. It provides campaigns, coupons, couponsets, push notifications, beacons, location tracking, analytics, and user management.
- **Type**: Android Library (AAR) published to Maven via Gradle.
- **Package namespace**: `ly.warp.sdk`
- **License**: BSD-2-Clause (Warply Ltd.) — all source files must retain the license header.
- **Repository**: Git-based (`warply_android_sdk_maven_plugin`)
---
## 2. Project Structure
```
warply_android_sdk_maven_plugin/ # Root project
├── build.gradle # Root build config (AGP 8.7.3, plugins)
├── settings.gradle # Includes: app, warply_android_sdk
├── gradle.properties # Gradle/Maven properties
├── scripts/
│ ├── publish-module.gradle # Module publishing script
│ └── publish-root.gradle # Root publishing script
├── app/ # Demo/host application
│ ├── build.gradle
│ ├── src/main/
│ │ ├── AndroidManifest.xml
│ │ ├── assets/warply.properties # SDK configuration file
│ │ └── java/warp/ly/android_sdk/
│ │ ├── WarplyAndroidSDKApplication.java
│ │ └── activities/
│ │ ├── BaseActivity.java
│ │ └── SplashActivity.java
├── warply_android_sdk/ # SDK Library Module (the main codebase)
│ ├── build.gradle # Library build config, dependencies, publishing
│ ├── proguard-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml # SDK manifest (permissions, services, receivers)
│ ├── java/ly/warp/sdk/
│ │ ├── Warply.java # ★ SDK Singleton entry point
│ │ ├── activities/ # UI Activities
│ │ ├── fragments/ # UI Fragments
│ │ ├── db/ # SQLite database layer
│ │ ├── dexter/ # Runtime permissions library (embedded)
│ │ ├── io/
│ │ │ ├── adapters/ # RecyclerView/ViewPager adapters
│ │ │ ├── callbacks/ # Async callback interfaces
│ │ │ ├── models/ # Data models (Campaign, Coupon, etc.)
│ │ │ ├── request/ # Custom Volley request types
│ │ │ └── volley/ # Custom Volley fork + Retrofit layer
│ │ ├── receivers/ # Broadcast receivers
│ │ ├── services/ # Background services (push, location, etc.)
│ │ ├── utils/ # Utility classes
│ │ │ ├── constants/ # WarpConstants, GCMConstants
│ │ │ └── managers/ # Business logic managers
│ │ └── views/ # Custom views and decorations
│ └── res/ # Resources (layouts, drawables, fonts, etc.)
```
---
## 3. Build Configuration
| Setting | Value |
|---------|-------|
| **Android Gradle Plugin** | 8.7.3 |
| **compileSdkVersion** | 35 |
| **buildToolsVersion** | 35.0.0 |
| **minSdkVersion** | 31 |
| **targetSdkVersion** | 35 |
| **Namespace** | `ly.warp.sdk` |
| **Java version** | Java 8+ (no Kotlin) |
| **Publishing** | Maven (single variant: release with sources) |
### Key Dependencies
| Category | Library | Version/Notes |
|----------|---------|---------------|
| UI | `androidx.appcompat:appcompat` | 1.7.1 |
| UI | `com.google.android.material:material` | 1.13.0 |
| Security | `androidx.security:security-crypto` | 1.1.0 |
| Networking | `com.squareup.retrofit2:retrofit` | 3.0.0 |
| Networking | `com.squareup.retrofit2:converter-gson` | 3.0.0 |
| Networking | `com.squareup.okhttp3:logging-interceptor` | 5.1.0 |
| Networking | Custom Volley fork (embedded in source) | — |
| Reactive | `io.reactivex.rxjava3:rxjava` | 3.1.8 |
| Reactive | `io.reactivex.rxjava3:rxandroid` | 3.0.2 |
| Async | `com.google.guava:guava` | 33.0.0-android |
| Events | `org.greenrobot:eventbus` | 3.3.1 |
| Images | `com.github.bumptech.glide:glide` | 4.16.0 |
| Firebase | `com.google.firebase:firebase-bom` | 34.2.0 |
| Firebase | `com.google.firebase:firebase-messaging` | (from BOM) |
| GMS | `com.google.android.gms:play-services-base` | 18.7.2 |
| GMS | `com.google.android.gms:play-services-location` | 21.3.0 |
| Huawei | `com.huawei.agconnect:agconnect-core` | 1.9.3.301 |
| Huawei | `com.huawei.hms:push` | 6.10.0.300 |
| Beacons | `org.altbeacon:android-beacon-library` | 2.19.3 |
| DB | `androidx.sqlite:sqlite` | 2.5.2 |
| Work | `androidx.work:work-runtime` | 2.10.3 |
| Lifecycle | `androidx.lifecycle:lifecycle-extensions` | 2.2.0 |
### SDK Configuration
The SDK is configured at runtime via a `warply.properties` file placed in the host app's `assets/` directory. Key properties include:
- `Uuid` — Application UUID (identifies the app to the Warply backend)
- `BaseURL` — Backend server base URL
- `Language` — Default language
- `Debug` — Enable/disable debug logging
- `MerchantId` — Merchant identifier
- `LoginType` — Authentication type
- `PushColor`, `PushIcon`, `PushSound` — Push notification customization
- `ProgressColor`, `ProgressDrawable` — UI loading customization
**Configuration is read via `WarplyProperty.java`** which loads properties from the assets file.
---
## 4. Architecture & Core Patterns
### 4.1 SDK Entry Point — `Warply.java` (Singleton Enum)
```java
public enum Warply {
INSTANCE;
// ...
}
```
- **Initialization**: `Warply.getInitializer(context)` or `Warply.getInitializer(context, callback)`
- Uses `WarplyInitializer` for setup, which calls `initInternal()` to configure the request queue, debug mode, server preferences, etc.
- **Registration**: `Warply.registerWarply()` — registers the device with the Warply backend
- **Context**: `Warply.getWarplyContext()` — returns the application context
- **Request Queue**: Internal Volley `RequestQueue` for legacy API calls
- **Microapp Pattern**: Data is posted/retrieved per "microapp" (e.g., `device_info`, `application_data`, `inapp_analytics`, `offers`, etc.)
**Key Pattern — Public static → private internal delegation:**
```java
public static void someMethod() {
INSTANCE.isInitializedOrThrow();
INSTANCE.someMethodInternal();
}
private void someMethodInternal() { /* actual logic */ }
```
### 4.2 API Manager — `WarplyManager.java`
The primary class for all backend API calls. Uses **static methods** exclusively.
**Key API Methods:**
| Method | Returns | Description |
|--------|---------|-------------|
| `login(email, receiver)` | `JSONObject` | Authenticate user |
| `logout(receiver)` | `JSONObject` | Logout and clear tokens |
| `getUser(receiver)` | `User` | Fetch user profile |
| `getCampaigns(receiver)` | `ArrayList<BannerItem>` | Fetch campaigns + articles (parallel) |
| `getCouponsets(receiver)` | `LinkedHashMap<String, ArrayList<Couponset>>` | Fetch couponsets + merchants (parallel, categorized) |
| `getUserCouponsWithCouponsets(receiver)` | `CouponList` | Fetch user's redeemed coupons |
| `getSingleCampaign(sessionUuid)` | void | Trigger campaign session |
**Async Pattern — Guava ListenableFuture for parallel requests:**
```java
ListenableFuture<ArrayList<Campaign>> futureCampaigns = getCampaignsRetro(service);
ListenableFuture<ArrayList<Content>> futureArticles = getArticlesRetro(service, categories);
ListenableFuture<List<Object>> allResultsFuture = Futures.allAsList(futureCampaigns, futureArticles);
ListenableFuture<ArrayList<BannerItem>> mergedResult = Futures.transformAsync(allResultsFuture, results -> {
// merge logic
}, executorService);
Futures.addCallback(mergedResult, new FutureCallback<>() {
public void onSuccess(result) { new Handler(Looper.getMainLooper()).post(() -> receiver.onSuccess(result)); }
public void onFailure(throwable) { new Handler(Looper.getMainLooper()).post(() -> receiver.onFailure(2)); }
}, executorService);
```
**Token Refresh Pattern:**
- On HTTP 401 → call `refreshToken()` → retry original request
- Max 3 retries (`MAX_RETRIES = 3`)
- On refresh failure with 401 → clear auth tokens and fail
### 4.3 In-Memory Cache — `WarplyManagerHelper.java`
Static fields caching the latest fetched data:
- `mCouponRedeemedList` — user's active coupons
- `mCampaignListAll` — all campaigns
- `mBannerListAll` — banner items (campaigns + articles merged)
- `mCouponsetCategorizedMap` — couponsets organized by category (`LinkedHashMap<String, ArrayList<Couponset>>`)
**Important**: These are set by `WarplyManager` after API calls and read by UI components.
### 4.4 Callback Interface
```java
public interface CallbackReceiver<T> {
void onSuccess(T result);
void onFailure(int errorCode);
}
```
All async operations use this pattern. Error codes are defined in `WarpConstants`:
- `1` = Success
- `2` = Generic error
- `401` = Unauthorized (triggers token refresh)
- `-1` = No internet
- `-4` = Not registered
---
## 5. Networking Layer
### 5.1 Retrofit (Modern API calls)
**`ApiClient.java`** — Singleton Retrofit instance:
- Base URL from `WarplyProperty.getBaseUrl()`
- OkHttp client with 30s connect/write/read timeouts
- GsonConverterFactory for serialization
**`ApiService.java`** — Retrofit interface defining all endpoints:
| Endpoint | Path | Auth |
|----------|------|------|
| Login | `POST /partners/dei/app_login` | No Bearer |
| Logout | `POST /oauth/{appUuid}/logout` | Bearer token |
| Logout (JWT) | `POST /user/v5/{appUuid}/logout` | Bearer token |
| Get User | `POST /oauth/{appUuid}/context` | Bearer token |
| Get Coupons | `POST /oauth/{appUuid}/context` | Bearer token |
| Get Campaigns | `POST /api/mobile/v2/{appUuid}/context/` | Signature only |
| Get Articles | `POST /api/mobile/v2/{appUuid}/context/` | Signature only |
| Get Campaigns (Personalized) | `POST /oauth/{appUuid}/context` | Bearer token |
| Get Couponsets | `POST /api/mobile/v2/{appUuid}/context/` | Bearer token |
| Get Merchants | `POST /api/mobile/v2/{appUuid}/context/` | Signature only |
**Required Headers on all requests:**
```
Content-Type: application/json
loyalty-date: <timestamp yyyy-MM-dd hh:mm:ss>
loyalty-bundle-id: android:<package_name>
unique-device-id: <device_unique_id>
channel: mobile
loyalty-web-id: <web_id>
loyalty-signature: SHA256(api_key + timestamp)
Authorization: Bearer <access_token> (when authenticated)
```
**Signature Generation** (`WarpUtils.produceSignature`):
```java
SHA-256(apiKey + timestamp) hex string
```
### 5.2 Custom Volley Fork (Legacy)
The project contains a **full embedded copy of Volley** under `io/volley/` with custom modifications. This is used for:
- Legacy microapp data posting (batched requests from DB queue)
- Context retrieval
- Device registration
**⚠️ Do NOT replace or upgrade this Volley fork** — it contains custom request types (`WarplyJsonObjectRequest`, `WarplyJsonArrayRequest`) and batching logic specific to the SDK.
---
## 6. Data Layer
### 6.1 SQLite Database — `WarplyDBHelper.java`
**Singleton pattern**: `WarplyDBHelper.getInstance(context)`
| Table | Purpose | Key Columns |
|-------|---------|-------------|
| `requests` | Queued microapp data requests | microapp, entity, force_post, date_added |
| `push_requests` | Queued push notification events | microapp, entity, force_post, date_added |
| `push_ack_requests` | Queued push acknowledgments | microapp, entity, force_post, date_added |
| `client` | OAuth client credentials | client_id (encrypted), client_secret (encrypted) |
| `auth` | OAuth tokens | access_token (encrypted), refresh_token (encrypted) |
| `tags` | User tags | tag, last_add_date |
**⚠️ CRITICAL**: Auth tokens and client credentials are stored with **field-level encryption** using `CryptoUtils.encrypt()`/`CryptoUtils.decrypt()`. Always use these methods when reading/writing to the `client` and `auth` tables.
### 6.2 Encrypted SharedPreferences — `WarpUtils.java`
Preferences are stored using `EncryptedSharedPreferences` with:
- `MasterKey` (AES256_GCM scheme)
- Key encryption: AES256_SIV
- Value encryption: AES256_GCM
- Fallback to plain `SharedPreferences` on error
Key stored values: web_id, api_key, device_token, device_info, app_data, locale, dark_mode, JWT enabled flag, webview params.
---
## 7. Data Models
All models are in `ly.warp.sdk.io.models` and follow these conventions:
### 7.1 Model Pattern
```java
public class ModelName implements Parcelable, Serializable {
private static final long serialVersionUID = ...;
// JSON key constants
private static final String FIELD_NAME = "json_key";
// Member variables
private String fieldName;
// Null-safe JSON helper
private static String optNullableString(JSONObject json, String key) {
return json.isNull(key) ? null : json.optString(key);
}
// Constructors: default, JSONObject, String (JSON), Parcel
public ModelName() { /* defaults */ }
public ModelName(JSONObject json) { /* parse from JSON */ }
public ModelName(String json) throws JSONException { this(new JSONObject(json)); }
public ModelName(Parcel source) { /* read from parcel */ }
// Serialization
public JSONObject toJSONObject() { /* to JSON */ }
public String toString() { return toJSONObject().toString(); }
public String toHumanReadableString() { return toJSONObject().toString(2); }
// Parcelable
public void writeToParcel(Parcel dest, int flags) { /* write */ }
public static final Creator<ModelName> CREATOR = ...;
// Getters and Setters
}
```
### 7.2 Key Models
| Model | Key Fields | Notes |
|-------|-----------|-------|
| **Campaign** | sessionUUID, title, subtitle, logoUrl, indexUrl, offerCategory, sorting, type, extraFields, actions, startDate, endDate | Represents loyalty campaigns |
| **Couponset** | uuid, name, description, imgPreview, merchantUuid, offerCategory, promoted, points, discount, availability, extraFields | Represents coupon offers |
| **Coupon** | barcode, coupon, couponsetUuid, merchantUuid, status, expiration, discount, couponsetDetails, merchantDetails, redeemDetails | User's acquired coupons |
| **Merchant** | uuid, name, address, logo, imgPreview, latitude, longitude, category, extraFields, merchantMetadata | Merchant/store info |
| **User** | (parsed from JSON result) | User profile data |
| **BannerItem** | Contains either Campaign or Content | Unified carousel item |
| **Content** | (articles/content items) | CMS content with imgPreview, extraFields |
| **CouponList** | Extends ArrayList\<Coupon\> | Typed list of coupons |
**⚠️ JSON Handling**: Models use `org.json.JSONObject`/`JSONArray`**NOT Gson**. Do not change this pattern. The `optNullableString()` helper must be used for nullable String fields.
---
## 8. UI Layer
### 8.1 Activities
| Activity | Purpose |
|----------|---------|
| `HomeActivity` | Main SDK screen — banner carousel + categorized couponset sections |
| `ProfileActivity` | User profile screen |
| `SingleCouponActivity` | Single coupon detail view |
| `SingleCouponsetActivity` | Single couponset/offer detail view |
| `WarpViewActivity` | WebView for campaign content (loads campaign URLs) |
| `BaseFragmentActivity` | Fragment host activity |
| `WarpBaseActivity` | Base activity class |
| `ApplicationSessionActivity` | Session management |
**UI Patterns:**
- Activities extend `android.app.Activity` (NOT `AppCompatActivity`) — except where noted
- Use `View.OnClickListener` interface implementation
- Navigation via `Intent` with `Parcelable` extras
- Loading states with `RelativeLayout` overlays (`mPbLoading`)
- Edge-to-edge display: `WarpUtils.applyEdgeToEdge(activity, topView, bottomView)`
### 8.2 Banner Carousel (HomeActivity)
- `ViewPager2` with `BannerAdapter`
- Pagination dots (custom `LinearLayout` with `ImageView` dots)
- Data source: `WarplyManagerHelper.getBannerList()` (merged campaigns + articles)
- Image preloading with Glide (`DiskCacheStrategy.DATA`)
- Click handlers: campaigns → `WarpViewActivity`, articles → `SingleCouponsetActivity` (via UUID lookup)
### 8.3 Couponset Sections (HomeActivity)
- Dynamically inflated sections from `item_couponset_section.xml`
- Each section: title + "See all" + horizontal `RecyclerView`
- `CouponsetAdapter` with `OnCouponsetClickListener`
- Max 5 items per section (`MAX_ITEMS_PER_SECTION = 5`)
- Data source: `LinkedHashMap<String, ArrayList<Couponset>>` — "Top offers" first, then by category
### 8.4 Campaign URL Construction
```java
// WarplyManagerHelper.constructCampaignUrl(campaign)
// Sets webview params as JSON in SharedPreferences:
{
"web_id": "...",
"app_uuid": "...",
"api_key": "...",
"session_uuid": "...",
"access_token": "...",
"refresh_token": "...",
"client_id": "...",
"client_secret": "...",
"lan": "...",
"dark": "true/false"
}
```
### 8.5 Fonts
Custom fonts in `res/font/`:
- `ping_lcg_bold.otf`
- `ping_lcg_regular.otf`
- `ping_lcg_light.otf`
Applied via: `WarpUtils.renderCustomFont(context, R.font.ping_lcg_bold, textView1, textView2, ...)`
---
## 9. Services & Background Work
| Service | Purpose |
|---------|---------|
| `FCMBaseMessagingService` | Firebase Cloud Messaging (currently commented out in manifest) |
| `HMSBaseMessagingService` | Huawei Push (currently commented out in manifest) |
| `UpdateUserLocationService` | Location updates (JobService) |
| `EventRefreshDeviceTokenService` | Device token refresh (JobService) |
| `WarplyBeaconsRangingService` | iBeacon ranging |
| `PushEventsWorkerService` | Push event processing (WorkManager) |
| `PushEventsClickedWorkerService` | Push click tracking (WorkManager) |
| Receiver | Purpose |
|----------|---------|
| `ConnectivityChangedReceiver` | Network state changes |
| `LocationChangedReceiver` | Location updates |
---
## 10. Code Style & Conventions
### 10.1 Section Comments
```java
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
// ===========================================================
// Methods
// ===========================================================
// ===========================================================
// Getter & Setter
// ===========================================================
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
```
### 10.2 License Header
Every source file must start with the BSD-2-Clause license block:
```java
/*
* Copyright 2010-2013 Warply Ltd. All rights reserved.
* ...BSD-2-Clause text...
*/
```
### 10.3 Naming Conventions
- **Packages**: `ly.warp.sdk.*`
- **Member variables**: `mPrefixedName` for UI fields (e.g., `mPbLoading`, `mBannerViewPager`)
- **Constants**: `UPPER_SNAKE_CASE` (e.g., `MAX_ITEMS_PER_SECTION`, `HEADER_SIGNATURE`)
- **JSON keys**: `lower_snake_case` as `private static final String` constants
- **Callbacks**: Anonymous inner classes or final field references (e.g., `mCampaignsCallback`)
### 10.4 Logging
```java
WarpUtils.log("message"); // Debug log (only when WarpConstants.DEBUG is true)
WarpUtils.verbose("message"); // Verbose log
WarpUtils.warn("message", exception); // Warning log
```
### 10.5 General Rules
- **Pure Java** — no Kotlin in the SDK module
- **No data binding** — traditional `findViewById()` approach
- **No dependency injection** — manual singleton/static patterns
- **Callbacks over coroutines** — all async uses `CallbackReceiver<T>`
- **Author attribution**: `Created by Panagiotis Triantafyllou on <date>.`
---
## 11. Critical Rules for AI Agents
### ⚠️ DO NOT:
1. **Do NOT use Gson for model serialization** — all models use `org.json.JSONObject`/`JSONArray`. The `converter-gson` dependency is only for Retrofit response parsing.
2. **Do NOT modify the embedded Volley fork** (`io/volley/`) — it contains customized request handling specific to the SDK's microapp architecture.
3. **Do NOT store tokens in plain text** — always use `CryptoUtils.encrypt()` when writing to the `client` or `auth` DB tables, and `CryptoUtils.decrypt()` when reading.
4. **Do NOT change Activities to AppCompatActivity** unless explicitly required — the SDK uses `android.app.Activity` base class.
5. **Do NOT introduce Kotlin** into the SDK module — it is a pure Java codebase.
6. **Do NOT hard-code server URLs, API keys, or UUIDs** — these come from `warply.properties` via `WarplyProperty`.
7. **Do NOT remove or modify the BSD license headers** from source files.
8. **Do NOT expose sensitive data** (tokens, keys, credentials) in logs — all logging is gated behind `WarpConstants.DEBUG`.
### ✅ DO:
1. **Follow the Parcelable + Serializable pattern** for all new data models.
2. **Use `optNullableString()` helper** for nullable String JSON fields in models.
3. **Use `CallbackReceiver<T>` pattern** for all new async operations.
4. **Use Guava `ListenableFuture`** with `Futures.allAsList()` for parallel API calls in `WarplyManager`.
5. **Post results to main thread** via `new Handler(Looper.getMainLooper()).post(...)` in Futures callbacks.
6. **Implement token refresh retry** (max 3 attempts) for authenticated endpoints.
7. **Use `WarpUtils.renderCustomFont()`** for applying custom fonts to TextViews.
8. **Use `WarpUtils.applyEdgeToEdge()`** for edge-to-edge display in new Activities.
9. **Cache API results** in `WarplyManagerHelper` static fields for UI consumption.
10. **Update `WarpConstants.SDK_VERSION`** and `build.gradle` `PUBLISH_VERSION` when making version changes.
---
## 12. Common Tasks Quick Reference
### Adding a new API endpoint:
1. Add Retrofit method to `ApiService.java`
2. Add static method in `WarplyManager.java` with `ListenableFuture` pattern
3. Create/update model in `io/models/`
4. Add cache field in `WarplyManagerHelper.java` if needed
5. Call from Activity/Fragment with `CallbackReceiver<T>`
### Adding a new Activity:
1. Create in `ly.warp.sdk.activities`
2. Extend `android.app.Activity`
3. Register in `warply_android_sdk/src/main/AndroidManifest.xml` with `android:exported="false"` and `android:theme="@style/SDKAppTheme"`
4. Use `WarpUtils.applyEdgeToEdge()` in `onCreate()`
5. Follow `mPrefixed` naming for view fields
### Adding a new data model:
1. Create in `ly.warp.sdk.io.models`
2. Implement `Parcelable, Serializable`
3. Define JSON key constants as `private static final String`
4. Add `optNullableString()` helper
5. Implement constructors: default, `JSONObject`, `String`, `Parcel`
6. Implement `toJSONObject()`, `toString()`, `toHumanReadableString()`
7. Implement `writeToParcel()` and `CREATOR`
8. Add getters/setters
### Debugging:
- Set `Debug=true` in `warply.properties` to enable `WarpUtils.log()` output
- Enable OkHttp logging interceptor in `ApiClient.getClient()` (uncomment the interceptor lines)
- Check `WARP_DEBUG` tag in Logcat
---
## 13. Manifest Permissions
The SDK requests these permissions:
```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
```
Runtime permissions are handled via the embedded **Dexter** library (`ly.warp.sdk.dexter`).
---
## 14. Version Management
- **SDK Version constant**: `WarpConstants.SDK_VERSION` (in `WarpConstants.java`)
- **Publish version**: `PUBLISH_VERSION` (in `warply_android_sdk/build.gradle`)
- **Group ID**: `ly.warp`
- **Artifact ID**: `warply-android-sdk`
- Both must be updated in sync when releasing new versions.
......@@ -5,7 +5,7 @@ android.buildFeatures.buildConfig = true
ext {
PUBLISH_GROUP_ID = 'ly.warp'
PUBLISH_VERSION = '4.5.5.4deh3'
PUBLISH_VERSION = '4.5.5.6deh4'
PUBLISH_ARTIFACT_ID = 'warply-android-sdk'
}
......
......@@ -45,6 +45,12 @@
android:theme="@style/SDKAppTheme" />
<activity
android:name=".activities.SingleCouponsetActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/SDKAppTheme" />
<activity
android:name=".activities.ProfileActivity"
android:exported="false"
android:screenOrientation="portrait"
......
......@@ -1343,8 +1343,6 @@ public enum Warply {
else {
if (warplyPath.equals("handle_image"))
sb = new StringBuilder(WarplyProperty.getBaseUrl(mContext) + WarpConstants.BASE_URL_API);
else if (warplyPath.equals("verify"))
sb = new StringBuilder(WarplyProperty.getBaseUrl(mContext) + WarplyProperty.getVerifyUrl(mContext));
else if (warplyPath.equals("cosuser"))
sb = new StringBuilder(WarplyProperty.getBaseUrl(mContext) + "/partners/oauth/" + WarplyProperty.getAppUuid(mContext) + "/token");
else
......
......@@ -3,6 +3,7 @@ package ly.warp.sdk.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
......@@ -93,9 +94,9 @@ public class HomeActivity extends Activity implements View.OnClickListener, Coup
@Override
public void onCouponsetClick(Couponset couponset, int position) {
// Intent myIntent = new Intent(HomeActivity.this, SingleCouponActivity.class);
// myIntent.putExtra(SingleCouponActivity.EXTRA_OFFER_ITEM, couponset);
// startActivity(myIntent);
Intent myIntent = new Intent(HomeActivity.this, SingleCouponsetActivity.class);
myIntent.putExtra(SingleCouponsetActivity.EXTRA_OFFER_ITEM, (Parcelable) couponset);
startActivity(myIntent);
}
// ===========================================================
......@@ -114,6 +115,69 @@ public class HomeActivity extends Activity implements View.OnClickListener, Coup
mIvProfile.setOnClickListener(this);
}
private void setupBannerCarousel() {
mBannerViewPager = findViewById(R.id.banner_viewpager);
mDotsContainer = findViewById(R.id.dots_container);
mBannerAdapter = new BannerAdapter(this, WarplyManagerHelper.getBannerList());
mBannerAdapter.setOnBannerCampaignClickListener(campaign -> {
startActivity(WarpViewActivity.createIntentFromURL(this, WarplyManagerHelper.constructCampaignUrl(campaign)));
});
mBannerAdapter.setOnBannerContentClickListener(article -> {
if (article != null && article.getExtraFields() != null) {
String couponsetUuid = article.getExtraFields().optString("url_link", null);
if (couponsetUuid != null && !couponsetUuid.isEmpty()) {
Couponset matchedCouponset = findCouponsetByUuid(couponsetUuid);
if (matchedCouponset != null) {
Intent myIntent = new Intent(HomeActivity.this, SingleCouponsetActivity.class);
myIntent.putExtra(SingleCouponsetActivity.EXTRA_OFFER_ITEM, (Parcelable) matchedCouponset);
startActivity(myIntent);
}
}
}
});
// Set the number of pages to preload for adjacent items
mBannerViewPager.setOffscreenPageLimit(5);
mBannerViewPager.setAdapter(mBannerAdapter);
setupPaginationDots(WarplyManagerHelper.getBannerList().size());
mBannerViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
updateDots(position);
}
});
}
private void setupPaginationDots(int count) {
mDots = new ArrayList<>();
mDotsContainer.removeAllViews();
for (int i = 0; i < count; i++) {
ImageView dot = new ImageView(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(8, 0, 8, 0);
dot.setLayoutParams(params);
dot.setImageResource(i == 0 ? R.drawable.dot_active : R.drawable.dot_inactive);
mDotsContainer.addView(dot);
mDots.add(dot);
}
}
private void updateDots(int position) {
for (int i = 0; i < mDots.size(); i++) {
mDots.get(i).setImageResource(i == position ? R.drawable.dot_active : R.drawable.dot_inactive);
}
}
private void setupCouponsetSections(LinkedHashMap<String, ArrayList<Couponset>> categorizedMap) {
mSectionsContainer.removeAllViews();
......@@ -163,67 +227,20 @@ public class HomeActivity extends Activity implements View.OnClickListener, Coup
mSectionsLoading.setVisibility(View.GONE);
}
// ===========================================================
// Banner Methods
// ===========================================================
private void setupBannerCarousel() {
mBannerViewPager = findViewById(R.id.banner_viewpager);
mDotsContainer = findViewById(R.id.dots_container);
mBannerAdapter = new BannerAdapter(this, WarplyManagerHelper.getBannerList());
mBannerAdapter.setOnBannerCampaignClickListener(campaign -> {
startActivity(WarpViewActivity.createIntentFromURL(this, WarplyManagerHelper.constructCampaignUrl(campaign)));
});
mBannerAdapter.setOnBannerContentClickListener(article -> {
//TODO: click article
});
// Set the number of pages to preload for adjacent items
mBannerViewPager.setOffscreenPageLimit(5);
mBannerViewPager.setAdapter(mBannerAdapter);
setupPaginationDots(WarplyManagerHelper.getBannerList().size());
mBannerViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
updateDots(position);
}
});
private Couponset findCouponsetByUuid(String uuid) {
LinkedHashMap<String, ArrayList<Couponset>> categorizedMap = WarplyManagerHelper.getCouponsetCategorizedMap();
if (categorizedMap != null) {
for (ArrayList<Couponset> couponsets : categorizedMap.values()) {
for (Couponset couponset : couponsets) {
if (uuid.equals(couponset.getUuid())) {
return couponset;
}
private void setupPaginationDots(int count) {
mDots = new ArrayList<>();
mDotsContainer.removeAllViews();
for (int i = 0; i < count; i++) {
ImageView dot = new ImageView(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(8, 0, 8, 0);
dot.setLayoutParams(params);
dot.setImageResource(i == 0 ? R.drawable.dot_active : R.drawable.dot_inactive);
mDotsContainer.addView(dot);
mDots.add(dot);
}
}
private void updateDots(int position) {
for (int i = 0; i < mDots.size(); i++) {
mDots.get(i).setImageResource(i == position ? R.drawable.dot_active : R.drawable.dot_inactive);
}
return null;
}
// ===========================================================
// Callbacks
// ===========================================================
private final CallbackReceiver<ArrayList<BannerItem>> mCampaignsCallback = new CallbackReceiver<ArrayList<BannerItem>>() {
@Override
public void onSuccess(ArrayList<BannerItem> result) {
......
......@@ -3,28 +3,28 @@ package ly.warp.sdk.activities;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowInsetsController;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.ArrayList;
import ly.warp.sdk.R;
import ly.warp.sdk.io.adapters.CouponAdapter;
import ly.warp.sdk.io.adapters.OfferAdapter;
import ly.warp.sdk.io.models.CouponItem;
import ly.warp.sdk.io.models.DummyDataProvider;
import ly.warp.sdk.io.models.OfferCategory;
import ly.warp.sdk.io.callbacks.CallbackReceiver;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.io.models.OfferItem;
import ly.warp.sdk.utils.WarpUtils;
import ly.warp.sdk.views.HorizontalSpaceItemDecoration;
import ly.warp.sdk.utils.WarplyManagerHelper;
import ly.warp.sdk.utils.managers.WarplyManager;
import ly.warp.sdk.views.VerticalSpaceItemDecoration;
public class ProfileActivity extends Activity implements View.OnClickListener, OfferAdapter.OnOfferClickListener, CouponAdapter.OnCouponClickListener {
......@@ -37,6 +37,7 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
// Fields
// ===========================================================
private RelativeLayout mPbLoading;
private TextView mTvHeaderTitle;
private ImageView mIvBack;
......@@ -51,7 +52,6 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
private RecyclerView mRvCoupons;
private CouponAdapter mCouponsAdapter;
private TextView mBtnFilterActive, mBtnFilterFavorites, mBtnFilterRedeemed;
private List<CouponItem> mCouponItems;
// ===========================================================
// Methods for/from SuperClass/Interfaces
......@@ -68,11 +68,8 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
findViewById(R.id.header_layout),
null);
// Setup profile suggestions section
setupProfileSuggestionsSection();
// Setup my coupons section
setupMyCouponsSection();
mPbLoading.setVisibility(View.VISIBLE);
WarplyManager.getCoupons(mCouponsCallback);
}
@Override
......@@ -86,19 +83,19 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
if (id == R.id.iv_back) {
onBackPressed();
} else if (id == R.id.btn_filter_active) {
filterCoupons(CouponItem.STATUS_ACTIVE);
// filterCoupons(CouponItem.STATUS_ACTIVE);
} else if (id == R.id.btn_filter_favorites) {
filterCoupons(CouponItem.STATUS_FAVORITE);
// filterCoupons(CouponItem.STATUS_FAVORITE);
} else if (id == R.id.btn_filter_redeemed) {
filterCoupons(CouponItem.STATUS_REDEEMED);
// filterCoupons(CouponItem.STATUS_REDEEMED);
}
}
@Override
public void onOfferClick(OfferItem offerItem, int position) {
Intent myIntent = new Intent(ProfileActivity.this, SingleCouponActivity.class);
myIntent.putExtra(SingleCouponActivity.EXTRA_OFFER_ITEM, offerItem);
startActivity(myIntent);
// Intent myIntent = new Intent(ProfileActivity.this, SingleCouponActivity.class);
// myIntent.putExtra(SingleCouponActivity.EXTRA_OFFER_ITEM, offerItem);
// startActivity(myIntent);
}
@Override
......@@ -107,14 +104,14 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
}
@Override
public void onCouponClick(CouponItem couponItem, int position) {
public void onCouponClick(Coupon couponItem, int position) {
Intent myIntent = new Intent(ProfileActivity.this, SingleCouponActivity.class);
myIntent.putExtra(SingleCouponActivity.EXTRA_OFFER_ITEM, couponItem);
myIntent.putExtra(SingleCouponActivity.EXTRA_OFFER_ITEM, (Parcelable) couponItem);
startActivity(myIntent);
}
@Override
public void onFavoriteClick(CouponItem couponItem, int position) {
public void onFavoriteClick(Coupon couponItem, int position) {
// Handle favorite click if needed
}
......@@ -126,6 +123,9 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
mIvBack = findViewById(R.id.iv_back);
mIvBack.setOnClickListener(this);
mPbLoading = findViewById(R.id.pb_loading);
mPbLoading.setOnTouchListener((v, event) -> true);
mTvHeaderTitle = findViewById(R.id.tv_header_title);
// Initialize Profile Suggestions section
......@@ -151,63 +151,22 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
mBtnFilterActive, mBtnFilterFavorites, mBtnFilterRedeemed);
}
/**
* Set up the Profile Suggestions section with dummy data
*/
private void setupProfileSuggestionsSection() {
// Get Profile Suggestions data
OfferCategory profileSuggestionsCategory = DummyDataProvider.getProfileSuggestions();
// Set category title with item count
String categoryTitle = profileSuggestionsCategory.getName() + " (" + profileSuggestionsCategory.getItems().size() + ")";
mTvCategoryProfileSuggestions.setText(categoryTitle);
// Set up RecyclerView
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
mRvProfileSuggestions.setLayoutManager(layoutManager);
// Add spacing between items
int spacingInPixels = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics());
mRvProfileSuggestions.addItemDecoration(new HorizontalSpaceItemDecoration(spacingInPixels));
// Create and set adapter
mProfileSuggestionsAdapter = new OfferAdapter(this, profileSuggestionsCategory.getItems());
mProfileSuggestionsAdapter.setOnOfferClickListener(this);
mRvProfileSuggestions.setAdapter(mProfileSuggestionsAdapter);
}
/**
* Set up the My Coupons section with dummy data and filters
*/
private void setupMyCouponsSection() {
// Get coupons data
mCouponItems = DummyDataProvider.getCoupons();
// Set up RecyclerView
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mRvCoupons.setLayoutManager(layoutManager);
mRvCoupons.setHasFixedSize(true);
// Create and set adapter
mCouponsAdapter = new CouponAdapter(this, mCouponItems);
mCouponsAdapter = new CouponAdapter(this, WarplyManagerHelper.getCoupons());
mCouponsAdapter.setOnCouponClickListener(this);
mRvCoupons.setAdapter(mCouponsAdapter);
// Add 16dp spacing between coupon items
int verticalSpacingInPixels = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
mRvCoupons.addItemDecoration(new VerticalSpaceItemDecoration(verticalSpacingInPixels));
// Filter by active coupons by default
filterCoupons(CouponItem.STATUS_ACTIVE);
// filterCoupons(CouponItem.STATUS_ACTIVE);
}
/**
* Filter coupons by status
*
* @param status The status to filter by
*/
private void filterCoupons(String status) {
// Reset all filter button styles
mBtnFilterActive.setBackgroundResource(R.drawable.shape_transparent_black_border);
......@@ -220,22 +179,35 @@ public class ProfileActivity extends Activity implements View.OnClickListener, O
mBtnFilterRedeemed.setTextColor(getResources().getColor(R.color.custom_black2));
// Set selected filter button style
if (CouponItem.STATUS_ACTIVE.equals(status)) {
mBtnFilterActive.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
mBtnFilterActive.setTextColor(Color.WHITE);
} else if (CouponItem.STATUS_FAVORITE.equals(status)) {
mBtnFilterFavorites.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
mBtnFilterFavorites.setTextColor(Color.WHITE);
} else if (CouponItem.STATUS_REDEEMED.equals(status)) {
mBtnFilterRedeemed.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
mBtnFilterRedeemed.setTextColor(Color.WHITE);
}
// if (CouponItem.STATUS_ACTIVE.equals(status)) {
// mBtnFilterActive.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
// mBtnFilterActive.setTextColor(Color.WHITE);
// } else if (CouponItem.STATUS_FAVORITE.equals(status)) {
// mBtnFilterFavorites.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
// mBtnFilterFavorites.setTextColor(Color.WHITE);
// } else if (CouponItem.STATUS_REDEEMED.equals(status)) {
// mBtnFilterRedeemed.setBackgroundResource(R.drawable.shape_rectangle_rounded_black);
// mBtnFilterRedeemed.setTextColor(Color.WHITE);
// }
// Apply filter to adapter
mCouponsAdapter.filterByStatus(status);
// mCouponsAdapter.filterByStatus(status);
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
private final CallbackReceiver<ArrayList<Coupon>> mCouponsCallback = new CallbackReceiver<ArrayList<Coupon>>() {
@Override
public void onSuccess(ArrayList<Coupon> result) {
mPbLoading.setVisibility(View.GONE);
setupMyCouponsSection();
}
@Override
public void onFailure(int errorCode) {
mPbLoading.setVisibility(View.GONE);
}
};
}
......
......@@ -5,32 +5,40 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowInsetsController;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.text.HtmlCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import ly.warp.sdk.R;
import ly.warp.sdk.io.models.OfferItem;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.utils.WarpUtils;
public class SingleCouponActivity extends Activity implements View.OnClickListener {
// ===========================================================
// Constants
// ===========================================================
public static final String EXTRA_OFFER_ITEM = "offer_item";
public static final String EXTRA_OFFER_ITEM = "coupon_item";
// ===========================================================
// Fields
// ===========================================================
private ImageView mIvBack;
private OfferItem mOfferItem;
private Coupon mOfferItem;
private TextView mTvSmallDescription;
private TextView mTvFullDescription;
private TextView mTvEndDate;
......@@ -80,10 +88,9 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_coupon);
// Get offer item from intent
Intent intent = getIntent();
if (intent != null && intent.hasExtra(EXTRA_OFFER_ITEM)) {
mOfferItem = (OfferItem) intent.getSerializableExtra(EXTRA_OFFER_ITEM);
mOfferItem = (Coupon) intent.getSerializableExtra(EXTRA_OFFER_ITEM);
}
initViews();
......@@ -151,34 +158,35 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
mTvEndDate, mTvFullDescription, mTvCouponCodeTitle, mTvQrCodeTitle, mTvTermsText,
mTVMoreTitle, mTvMoreButton);
// Populate views with offer data
if (mOfferItem != null) {
mTvValue.setText(mOfferItem.getValue());
// mTvSmallDescription.setText(mOfferItem.getDescription());
// Store the full description text
mFullDescriptionText = mOfferItem.getFullDescription();
mTvFullDescription.setText(mFullDescriptionText);
// Format and set the end date
String endDate = mOfferItem.getEndDate();
if (endDate != null && !endDate.isEmpty()) {
// Convert from DD/MM/YYYY to DD-MM-YYYY
String formattedDate = endDate.replace("/", "-");
if (mOfferItem.getExpiration() != null && !mOfferItem.getExpiration().isEmpty()) {
String formattedDate = formatValidityDate(mOfferItem.getExpiration());
mTvEndDate.setText(getString(R.string.demo_valid_until, formattedDate));
mTvEndDate.setVisibility(View.VISIBLE);
} else {
mTvEndDate.setVisibility(View.GONE);
}
// Load image (in a real app, you would use an image loading library)
// For demo purposes, we'll use a placeholder
int imageResId = getResources().getIdentifier(
mOfferItem.getImageUrl().replace(".png", ""),
"drawable",
getPackageName());
if (imageResId != 0) {
mIvImage.setImageResource(imageResId);
if (mOfferItem.getCouponsetDetails() != null && mOfferItem.getCouponsetDetails().getImg_preview() != null && !TextUtils.isEmpty(mOfferItem.getCouponsetDetails().getImg_preview())) {
Glide.with(this)
// .setDefaultRequestOptions(
// RequestOptions
// .placeholderOf(R.drawable.demo_logo)
// .error(R.drawable.demo_logo))
.load(mOfferItem.getCouponsetDetails().getImg_preview())
.diskCacheStrategy(DiskCacheStrategy.DATA)
.into(mIvImage);
}
if (mOfferItem.getCouponsetDetails() != null && mOfferItem.getCouponsetDetails().getName() != null && !TextUtils.isEmpty(mOfferItem.getCouponsetDetails().getName()))
mTvValue.setText(mOfferItem.getCouponsetDetails().getName());
if (mOfferItem.getCouponsetDetails() != null && mOfferItem.getCouponsetDetails().getShort_description() != null && !TextUtils.isEmpty(mOfferItem.getCouponsetDetails().getShort_description()))
mTvSmallDescription.setText(mOfferItem.getCouponsetDetails().getShort_description());
if (mOfferItem.getCouponsetDetails() != null && mOfferItem.getCouponsetDetails().getDescription() != null && !TextUtils.isEmpty(mOfferItem.getCouponsetDetails().getDescription()))
mTvFullDescription.setText(HtmlCompat.fromHtml(mOfferItem.getCouponsetDetails().getDescription(), HtmlCompat.FROM_HTML_MODE_COMPACT));
if (mOfferItem.getCouponsetDetails() != null && mOfferItem.getCouponsetDetails().getTerms() != null && !TextUtils.isEmpty(mOfferItem.getCouponsetDetails().getTerms()))
mTvTermsText.setText(HtmlCompat.fromHtml(mOfferItem.getCouponsetDetails().getTerms(), HtmlCompat.FROM_HTML_MODE_COMPACT));
// Setup the More button
setupMoreButton();
......@@ -193,18 +201,26 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
}
}
/**
* Sets up the coupon code expandable section
*/
private void setupCouponCodeSection() {
// Set coupon code - using a hardcoded value for demo purposes
// In a real app, this would come from the offer item
String couponCode = "coupons_ab";
if (mOfferItem != null && mOfferItem.getId() != null) {
// Use offer ID as part of the coupon code for demo purposes
couponCode = "coupon_" + mOfferItem.getId().toLowerCase();
private String formatValidityDate(String endDate) {
try {
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date date = inputFormat.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e) {
try {
SimpleDateFormat inputFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
Date date = inputFormat2.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e2) {
return endDate;
}
}
}
mTvCouponCode.setText(couponCode);
private void setupCouponCodeSection() {
mTvCouponCode.setText(mOfferItem.getCoupon());
// Set click listener for the header to expand/collapse
mCouponCodeHeader.setOnClickListener(new View.OnClickListener() {
......@@ -231,9 +247,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
});
}
/**
* Toggles between expanded and collapsed states for the coupon code section
*/
private void toggleCouponCodeExpansion() {
if (mIsCouponCodeExpanded) {
// Collapse the content
......@@ -248,9 +261,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
}
}
/**
* Copies the coupon code to the clipboard
*/
private void copyCouponCodeToClipboard() {
String couponCode = mTvCouponCode.getText().toString();
......@@ -267,9 +277,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
Toast.makeText(this, R.string.demo_copy_success, Toast.LENGTH_SHORT).show();
}
/**
* Sets up the "More" button for expanding/collapsing the description text
*/
private void setupMoreButton() {
// Wait for layout to be ready to check if text is truncated
mTvFullDescription.post(new Runnable() {
......@@ -292,9 +299,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
});
}
/**
* Toggles between expanded and collapsed states for the description text
*/
private void toggleDescriptionExpansion() {
if (mIsDescriptionExpanded) {
// Collapse the text
......@@ -309,9 +313,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
}
}
/**
* Sets up the QR code expandable section
*/
private void setupQrCodeSection() {
// Set click listener for the header to expand/collapse
mQrCodeHeader.setOnClickListener(new View.OnClickListener() {
......@@ -330,9 +331,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
});
}
/**
* Toggles between expanded and collapsed states for the QR code section
*/
private void toggleQrCodeExpansion() {
if (mIsQrCodeExpanded) {
// Collapse the content
......@@ -347,9 +345,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
}
}
/**
* Sets up the Terms of Use expandable section
*/
private void setupTermsSection() {
// Set click listener for the header to expand/collapse
mTermsHeader.setOnClickListener(new View.OnClickListener() {
......@@ -368,9 +363,6 @@ public class SingleCouponActivity extends Activity implements View.OnClickListen
});
}
/**
* Toggles between expanded and collapsed states for the Terms of Use section
*/
private void toggleTermsExpansion() {
if (mIsTermsExpanded) {
// Collapse the content
......
package ly.warp.sdk.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.core.text.HtmlCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import ly.warp.sdk.R;
import ly.warp.sdk.io.models.Couponset;
import ly.warp.sdk.utils.WarpUtils;
public class SingleCouponsetActivity extends Activity implements View.OnClickListener {
// ===========================================================
// Constants
// ===========================================================
public static final String EXTRA_OFFER_ITEM = "offer_item";
// ===========================================================
// Fields
// ===========================================================
private ImageView mIvBack;
private Couponset mOfferItem;
private TextView mTvSmallDescription;
private TextView mTvFullDescription;
private TextView mTvEndDate;
private TextView mTvValue;
private TextView mTvMoreButton;
private ImageView mIvImage;
private boolean mIsDescriptionExpanded = false;
// Terms of Use section
private LinearLayout mTermsContainer;
private LinearLayout mTermsHeader;
private LinearLayout mTermsContent;
private ImageView mIvTermsArrow;
private TextView mTvTermsText;
private boolean mIsTermsExpanded = false;
private TextView mTvHeaderTitle, mTvTermsTitle, mTvShopsTitle, mTvWebsiteTitle, mTVMoreTitle;
// ===========================================================
// Methods for/from SuperClass/Interfaces
// ===========================================================
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_couponset);
Intent intent = getIntent();
if (intent != null && intent.hasExtra(EXTRA_OFFER_ITEM)) {
mOfferItem = (Couponset) intent.getSerializableExtra(EXTRA_OFFER_ITEM);
}
initViews();
WarpUtils.applyEdgeToEdge(this,
findViewById(R.id.header_layout),
null);
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.iv_back) {
onBackPressed();
}
}
// ===========================================================
// Methods
// ===========================================================
private void initViews() {
mIvBack = findViewById(R.id.iv_back);
mIvBack.setOnClickListener(this);
// Initialize views
mTvSmallDescription = findViewById(R.id.tv_coupon_small_description);
mTvFullDescription = findViewById(R.id.tv_coupon_full_description);
mTvEndDate = findViewById(R.id.tv_coupon_end_date);
mTvValue = findViewById(R.id.tv_coupon_value);
mIvImage = findViewById(R.id.iv_coupon_image);
mTvMoreButton = findViewById(R.id.tv_more_button);
// Initialize Terms of Use section
mTermsContainer = findViewById(R.id.terms_container);
mTermsHeader = findViewById(R.id.terms_header);
mTermsContent = findViewById(R.id.terms_content);
mIvTermsArrow = findViewById(R.id.iv_terms_arrow);
mTvTermsText = findViewById(R.id.tv_terms_text);
mTvHeaderTitle = findViewById(R.id.tv_header_title);
mTvTermsTitle = findViewById(R.id.tv_terms_title);
mTvShopsTitle = findViewById(R.id.tv_shops_title);
mTvWebsiteTitle = findViewById(R.id.tv_website_title);
mTVMoreTitle = findViewById(R.id.tv_more_title);
WarpUtils.renderCustomFont(this, R.font.ping_lcg_bold, mTvHeaderTitle, mTvValue,
mTvTermsTitle, mTvShopsTitle, mTvWebsiteTitle);
WarpUtils.renderCustomFont(this, R.font.ping_lcg_regular, mTvSmallDescription,
mTvEndDate, mTvFullDescription, mTvTermsText,
mTVMoreTitle, mTvMoreButton);
if (mOfferItem != null) {
if (mOfferItem.getEndDate() != null && !mOfferItem.getEndDate().isEmpty()) {
String formattedDate = formatValidityDate(mOfferItem.getEndDate());
mTvEndDate.setText(getString(R.string.demo_valid_until, formattedDate));
mTvEndDate.setVisibility(View.VISIBLE);
} else {
mTvEndDate.setVisibility(View.GONE);
}
if (!TextUtils.isEmpty(mOfferItem.getImg_preview())) {
Glide.with(this)
// .setDefaultRequestOptions(
// RequestOptions
// .placeholderOf(R.drawable.demo_logo)
// .error(R.drawable.demo_logo))
.load(mOfferItem.getImg_preview())
.diskCacheStrategy(DiskCacheStrategy.DATA)
.into(mIvImage);
}
if (!TextUtils.isEmpty(mOfferItem.getName()))
mTvValue.setText(mOfferItem.getName());
if (!TextUtils.isEmpty(mOfferItem.getShort_description()))
mTvSmallDescription.setText(mOfferItem.getShort_description());
if (!TextUtils.isEmpty(mOfferItem.getDescription()))
mTvFullDescription.setText(HtmlCompat.fromHtml(mOfferItem.getDescription(), HtmlCompat.FROM_HTML_MODE_COMPACT));
if (!TextUtils.isEmpty(mOfferItem.getTerms()))
mTvTermsText.setText(HtmlCompat.fromHtml(mOfferItem.getTerms(), HtmlCompat.FROM_HTML_MODE_COMPACT));
// Setup the More button
setupMoreButton();
// Setup Terms of Use section
setupTermsSection();
}
}
private void setupMoreButton() {
// Wait for layout to be ready to check if text is truncated
mTvFullDescription.post(new Runnable() {
@Override
public void run() {
// Check if text is truncated (more than 4 lines)
if (mTvFullDescription.getLineCount() > 3) {
// Show the More button
mTvMoreButton.setVisibility(View.VISIBLE);
// Set click listener for the More button
mTvMoreButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleDescriptionExpansion();
}
});
}
}
});
}
private void toggleDescriptionExpansion() {
if (mIsDescriptionExpanded) {
// Collapse the text
mTvFullDescription.setMaxLines(4);
mTvMoreButton.setText(R.string.demo_more);
mIsDescriptionExpanded = false;
} else {
// Expand the text
mTvFullDescription.setMaxLines(Integer.MAX_VALUE);
mTvMoreButton.setText(R.string.demo_less);
mIsDescriptionExpanded = true;
}
}
private void setupTermsSection() {
// Set click listener for the header to expand/collapse
mTermsHeader.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleTermsExpansion();
}
});
// Set click listener for the entire container as well
mTermsContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleTermsExpansion();
}
});
}
private void toggleTermsExpansion() {
if (mIsTermsExpanded) {
// Collapse the content
mTermsContent.setVisibility(View.GONE);
mIvTermsArrow.setImageResource(R.drawable.ic_arrow_down);
mIsTermsExpanded = false;
} else {
// Expand the content
mTermsContent.setVisibility(View.VISIBLE);
mIvTermsArrow.setImageResource(R.drawable.ic_arrow_up);
mIsTermsExpanded = true;
}
}
private String formatValidityDate(String endDate) {
try {
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date date = inputFormat.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e) {
try {
SimpleDateFormat inputFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
Date date = inputFormat2.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e2) {
return endDate;
}
}
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}
package ly.warp.sdk.io.adapters;
import android.content.Context;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
......@@ -12,17 +13,17 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import ly.warp.sdk.R;
import ly.warp.sdk.io.models.CouponItem;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.utils.TopRoundedCornersTransformation;
import ly.warp.sdk.utils.WarpUtils;
......@@ -31,18 +32,19 @@ import ly.warp.sdk.utils.WarpUtils;
*/
public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.CouponViewHolder> {
private final List<CouponItem> allCouponItems;
private List<CouponItem> filteredCouponItems;
private final ArrayList<Coupon> allCouponItems;
private ArrayList<Coupon> filteredCouponItems;
private final Context context;
private OnCouponClickListener listener;
private String currentFilter = null;
private int currentFilter = 0;
/**
* Interface for handling coupon item clicks
*/
public interface OnCouponClickListener {
void onCouponClick(CouponItem couponItem, int position);
void onFavoriteClick(CouponItem couponItem, int position);
void onCouponClick(Coupon couponItem, int position);
void onFavoriteClick(Coupon couponItem, int position);
}
/**
......@@ -51,10 +53,10 @@ public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.CouponView
* @param context The context
* @param couponItems List of coupon items to display
*/
public CouponAdapter(Context context, List<CouponItem> couponItems) {
public CouponAdapter(Context context, ArrayList<Coupon> couponItems) {
this.context = context;
this.allCouponItems = couponItems;
this.filteredCouponItems = new ArrayList<>(couponItems);
this.filteredCouponItems = couponItems;
}
/**
......@@ -71,17 +73,15 @@ public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.CouponView
*
* @param status The status to filter by (active, favorite, redeemed) or null for all
*/
public void filterByStatus(String status) {
public void filterByStatus(int status) {
currentFilter = status;
filteredCouponItems.clear();
if (status == null) {
// Show all coupons
if (status == 0) {
filteredCouponItems.addAll(allCouponItems);
} else {
// Filter by status
for (CouponItem coupon : allCouponItems) {
if (status.equals(coupon.getStatus())) {
for (Coupon coupon : allCouponItems) {
if (status == coupon.getStatus()) {
filteredCouponItems.add(coupon);
}
}
......@@ -99,7 +99,7 @@ public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.CouponView
@Override
public void onBindViewHolder(@NonNull CouponViewHolder holder, int position) {
CouponItem couponItem = filteredCouponItems.get(position);
Coupon couponItem = filteredCouponItems.get(position);
holder.bind(couponItem, position);
}
......@@ -149,102 +149,71 @@ public class CouponAdapter extends RecyclerView.Adapter<CouponAdapter.CouponView
});
}
void bind(CouponItem couponItem, int position) {
// Set coupon data to views
tvTitle.setText(couponItem.getTitle());
tvDescription.setText(couponItem.getDescription());
tvPrice.setText(couponItem.getValue());
tvValidity.setText(formatValidityDate(couponItem.getEndDate()));
// Set heart icon based on favorite status
if (couponItem.isFavorite()) {
// Use pressed/filled heart for Favorites
ivFavorite.setImageResource(R.drawable.demo_heart_pressed);
void bind(Coupon couponItem, int position) {
tvTitle.setText(!TextUtils.isEmpty(couponItem.getCouponsetDetails().getName()) ? couponItem.getCouponsetDetails().getName() : "");
tvDescription.setText(!TextUtils.isEmpty(couponItem.getCouponsetDetails().getShort_description()) ? couponItem.getCouponsetDetails().getShort_description() : "");
tvPrice.setText(!TextUtils.isEmpty(couponItem.getDiscount()) ? couponItem.getDiscount() : "");
if (couponItem.getCouponsetDetails().getEndDate() != null && !couponItem.getCouponsetDetails().getEndDate().isEmpty()) {
tvValidity.setText(formatValidityDate(couponItem.getCouponsetDetails().getEndDate()));
tvValidity.setVisibility(View.VISIBLE);
} else {
// Use default/empty heart for other statuses
ivFavorite.setImageResource(R.drawable.demo_heart);
tvValidity.setVisibility(View.GONE);
}
// Load images from resources
loadOfferImage(couponItem.getImageUrl());
loadLogoImage(couponItem.getLogoUrl());
// Set heart icon based on favorite status
// if (couponItem.isFavorite()) {
// // Use pressed/filled heart for Favorites
// ivFavorite.setImageResource(R.drawable.demo_heart_pressed);
// } else {
// // Use default/empty heart for other statuses
// ivFavorite.setImageResource(R.drawable.demo_heart);
// }
loadCouponImage(couponItem.getCouponsetDetails().getImg_preview());
loadMerchantLogo(couponItem.getMerchantDetails().getImgPreview());
}
/**
* Format the end date to "έως dd-MM" format
*
* @param endDate The end date in "dd/MM/yyyy" format
* @return Formatted date string
*/
private String formatValidityDate(String endDate) {
try {
SimpleDateFormat inputFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault());
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date date = inputFormat.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e) {
// Fallback to original if parsing fails
try {
SimpleDateFormat inputFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
Date date = inputFormat2.parse(endDate);
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM", Locale.getDefault());
return "έως " + outputFormat.format(date);
} catch (ParseException e2) {
return endDate;
}
}
/**
* Load offer image with rounded top corners using Glide
*
* @param imageName The image resource name
*/
private void loadOfferImage(String imageName) {
try {
// Remove file extension if present
if (imageName.contains(".")) {
imageName = imageName.substring(0, imageName.lastIndexOf('.'));
}
// Get resource ID by name
int resourceId = context.getResources().getIdentifier(
imageName, "drawable", context.getPackageName());
if (resourceId != 0) {
// Convert 9dp to pixels
private void loadCouponImage(String imageUrl) {
if (imageUrl != null && !imageUrl.isEmpty()) {
int radiusInPixels = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 9,
context.getResources().getDisplayMetrics());
// Load with Glide and apply transformations
Glide.with(context)
.load(resourceId)
.load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.transform(new CenterCrop(), new TopRoundedCornersTransformation(radiusInPixels))
.into(ivOfferImage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Load logo image without transformations
*
* @param imageName The image resource name
*/
private void loadLogoImage(String imageName) {
try {
// Remove file extension if present
if (imageName.contains(".")) {
imageName = imageName.substring(0, imageName.lastIndexOf('.'));
}
// Get resource ID by name
int resourceId = context.getResources().getIdentifier(
imageName, "drawable", context.getPackageName());
if (resourceId != 0) {
// Load logo normally without transformations
ivLogo.setImageResource(resourceId);
}
} catch (Exception e) {
e.printStackTrace();
private void loadMerchantLogo(String logoUrl) {
if (logoUrl != null && !logoUrl.isEmpty()) {
ivLogo.setVisibility(View.VISIBLE);
Glide.with(context)
.load(logoUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.into(ivLogo);
} else {
ivLogo.setVisibility(View.GONE);
}
}
}
......
......@@ -73,71 +73,81 @@ public class Coupon implements Parcelable, Serializable {
/* Member variables of the Campaign object */
private String barcode = "";
private String category = "";
private String coupon = "";
private String created = "";
private String description = "";
private String discount = "";
private String expiration = "";
private String image = "";
private String name = "";
private String barcode = null;
private String category = null;
private String coupon = null;
private String created = null;
private String description = null;
private String discount = null;
private String expiration = null;
private String image = null;
private String name = null;
private int status = 0;
private String transactionDate = "";
private String transactionUuid = "";
private JSONObject changesDates = new JSONObject();
private String couponsetUuid = "";
private String merchantUuid = "";
private String innerText = "";
private String transactionDate = null;
private String transactionUuid = null;
private JSONObject changesDates = null;
private String couponsetUuid = null;
private String merchantUuid = null;
private String innerText = null;
private Date expirationDate = new Date();
private Date redeemDate = new Date();
private String discount_type = "";
private String discount_type = null;
private double final_price = 0.0d;
private String short_description = "";
private String terms = "";
private Couponset couponsetDetails = new Couponset(true);
private Merchant merchantDetails = new Merchant(true);
private RedeemMerchantDetails redeemDetails = new RedeemMerchantDetails();
private String short_description = null;
private String terms = null;
private Couponset couponsetDetails = null;
private Merchant merchantDetails = null;
private RedeemMerchantDetails redeemDetails = null;
/**
* Helper method to get a nullable String from a JSONObject.
* Returns null if the JSON value is null, otherwise returns the String value.
*/
private static String optNullableString(JSONObject json, String key) {
return json.isNull(key) ? null : json.optString(key);
}
public Coupon() {
this.barcode = "";
this.category = "";
this.coupon = "";
this.created = "";
this.description = "";
this.discount = "";
this.expiration = "";
this.image = "";
this.name = "";
this.barcode = null;
this.category = null;
this.coupon = null;
this.created = null;
this.description = null;
this.discount = null;
this.expiration = null;
this.image = null;
this.name = null;
this.status = 0;
this.transactionDate = "";
this.transactionUuid = "";
this.changesDates = new JSONObject();
this.couponsetUuid = "";
this.merchantUuid = "";
this.innerText = "";
this.transactionDate = null;
this.transactionUuid = null;
this.changesDates = null;
this.couponsetUuid = null;
this.merchantUuid = null;
this.innerText = null;
this.expirationDate = new Date();
this.redeemDate = new Date();
this.discount_type = "";
this.discount_type = null;
this.final_price = 0.0d;
this.short_description = "";
this.terms = "";
this.redeemDetails = new RedeemMerchantDetails();
this.short_description = null;
this.terms = null;
this.couponsetDetails = null;
this.merchantDetails = null;
this.redeemDetails = null;
}
public Coupon(boolean isUniversal) {
this.barcode = "";
this.coupon = "";
this.discount = "";
this.expiration = "";
this.barcode = null;
this.coupon = null;
this.discount = null;
this.expiration = null;
this.status = 0;
this.changesDates = new JSONObject();
this.couponsetUuid = "";
this.merchantUuid = "";
this.changesDates = null;
this.couponsetUuid = null;
this.merchantUuid = null;
this.redeemDate = new Date();
this.couponsetDetails = new Couponset(isUniversal);
this.merchantDetails = new Merchant(isUniversal);
this.redeemDetails = new RedeemMerchantDetails();
this.couponsetDetails = null;
this.merchantDetails = null;
this.redeemDetails = null;
}
/**
......@@ -158,21 +168,21 @@ public class Coupon implements Parcelable, Serializable {
*/
public Coupon(JSONObject json) {
if (json != null) {
this.barcode = json.optString(BARCODE);
this.category = json.optString(CATEGORY);
this.coupon = json.optString(COUPON);
this.created = json.optString(CREATED);
this.description = json.optString(DESCRIPTION);
this.discount = json.optString(DISCOUNT);
if (this.discount.contains(",")) {
this.barcode = optNullableString(json, BARCODE);
this.category = optNullableString(json, CATEGORY);
this.coupon = optNullableString(json, COUPON);
this.created = optNullableString(json, CREATED);
this.description = optNullableString(json, DESCRIPTION);
this.discount = optNullableString(json, DISCOUNT);
if (this.discount != null && this.discount.contains(",")) {
this.discount = this.discount.replace(",", ".");
}
this.expiration = json.optString(EXPIRATION);
this.image = json.optString(IMAGE);
this.name = json.optString(NAME);
this.expiration = optNullableString(json, EXPIRATION);
this.image = optNullableString(json, IMAGE);
this.name = optNullableString(json, NAME);
this.status = json.optInt(STATUS);
this.transactionDate = json.optString(TRANSACTION_DATE);
this.transactionUuid = json.optString(TRANSACTION_UUID);
this.transactionDate = optNullableString(json, TRANSACTION_DATE);
this.transactionUuid = optNullableString(json, TRANSACTION_UUID);
this.changesDates = json.optJSONObject(CHANGES_DATES);
if (this.changesDates != null) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
......@@ -185,13 +195,13 @@ public class Coupon implements Parcelable, Serializable {
e.printStackTrace();
}
}
this.couponsetUuid = json.optString(COUPONSET_UUID);
this.merchantUuid = json.optString(MERCHANT_UUID);
this.innerText = json.optString(INNER_TEXT);
this.discount_type = json.isNull(DISCOUNT_TYPE) ? "" : json.optString(DISCOUNT_TYPE);
this.final_price = json.optDouble(FINAL_PRICE);
this.short_description = json.optString(SHORT_DESCRIPTION);
this.terms = json.optString(TERMS);
this.couponsetUuid = optNullableString(json, COUPONSET_UUID);
this.merchantUuid = optNullableString(json, MERCHANT_UUID);
this.innerText = optNullableString(json, INNER_TEXT);
this.discount_type = optNullableString(json, DISCOUNT_TYPE);
this.final_price = json.isNull(FINAL_PRICE) ? 0.0d : json.optDouble(FINAL_PRICE);
this.short_description = optNullableString(json, SHORT_DESCRIPTION);
this.terms = optNullableString(json, TERMS);
JSONObject tempRedeemDetails = json.optJSONObject("redeemed_merchant_details");
if (tempRedeemDetails != null) {
this.redeemDetails = new RedeemMerchantDetails(tempRedeemDetails);
......@@ -201,7 +211,7 @@ public class Coupon implements Parcelable, Serializable {
public Coupon(JSONObject json, boolean isUniversal) {
if (json != null) {
this.barcode = json.optString(BARCODE);
this.barcode = optNullableString(json, BARCODE);
this.changesDates = json.optJSONObject(CHANGES_DATES);
if (this.changesDates != null) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
......@@ -214,14 +224,14 @@ public class Coupon implements Parcelable, Serializable {
e.printStackTrace();
}
}
this.coupon = json.optString(COUPON);
this.couponsetUuid = json.optString(COUPONSET_UUID);
this.discount = json.optString(DISCOUNT);
if (this.discount.contains(",")) {
this.coupon = optNullableString(json, COUPON);
this.couponsetUuid = optNullableString(json, COUPONSET_UUID);
this.discount = optNullableString(json, DISCOUNT);
if (this.discount != null && this.discount.contains(",")) {
this.discount = this.discount.replace(",", ".");
}
this.expiration = json.optString(EXPIRATION);
this.merchantUuid = json.optString(MERCHANT_UUID);
this.expiration = optNullableString(json, EXPIRATION);
this.merchantUuid = optNullableString(json, MERCHANT_UUID);
this.status = json.optInt(STATUS);
JSONObject tempCouponsetDetails = json.optJSONObject("couponset_details");
if (tempCouponsetDetails != null) {
......@@ -235,19 +245,6 @@ public class Coupon implements Parcelable, Serializable {
if (tempRedeemDetails != null) {
this.redeemDetails = new RedeemMerchantDetails(tempRedeemDetails);
}
// this.category = json.optString(CATEGORY);
// this.created = json.optString(CREATED);
// this.description = json.optString(DESCRIPTION);
// this.image = json.optString(IMAGE);
// this.name = json.optString(NAME);
// this.transactionDate = json.optString(TRANSACTION_DATE);
// this.transactionUuid = json.optString(TRANSACTION_UUID);
// this.innerText = json.optString(INNER_TEXT);
// this.discount_type = json.isNull(DISCOUNT_TYPE) ? "" : json.optString(DISCOUNT_TYPE);
// this.final_price = json.optDouble(FINAL_PRICE);
// this.short_description = json.optString(SHORT_DESCRIPTION);
// this.terms = json.optString(TERMS);
}
}
......@@ -258,24 +255,24 @@ public class Coupon implements Parcelable, Serializable {
private static final String REDEEMED_DATE = "redeemed_date";
private String imgPreview = "";
private String name = "";
private String uuid = "";
private String redeemedDate = "";
private String imgPreview = null;
private String name = null;
private String uuid = null;
private String redeemedDate = null;
public RedeemMerchantDetails() {
this.imgPreview = "";
this.name = "";
this.uuid = "";
this.redeemedDate = "";
this.imgPreview = null;
this.name = null;
this.uuid = null;
this.redeemedDate = null;
}
public RedeemMerchantDetails(JSONObject json) {
if (json != null) {
this.imgPreview = json.isNull(IMG_PREVIEW) ? "" : json.optString(IMG_PREVIEW);
this.name = json.isNull(NAME) ? "" : json.optString(NAME);
this.uuid = json.isNull(UUID) ? "" : json.optString(UUID);
this.redeemedDate = json.isNull(REDEEMED_DATE) ? "" : json.optString(REDEEMED_DATE);
this.imgPreview = json.isNull(IMG_PREVIEW) ? null : json.optString(IMG_PREVIEW);
this.name = json.isNull(NAME) ? null : json.optString(NAME);
this.uuid = json.isNull(UUID) ? null : json.optString(UUID);
this.redeemedDate = json.isNull(REDEEMED_DATE) ? null : json.optString(REDEEMED_DATE);
}
}
......@@ -332,6 +329,23 @@ public class Coupon implements Parcelable, Serializable {
this.final_price = source.readDouble();
this.short_description = source.readString();
this.terms = source.readString();
try {
String changesDatesStr = source.readString();
this.changesDates = changesDatesStr != null ? new JSONObject(changesDatesStr) : null;
} catch (JSONException e) {
this.changesDates = null;
}
this.couponsetDetails = source.readParcelable(Couponset.class.getClassLoader());
this.merchantDetails = source.readParcelable(Merchant.class.getClassLoader());
// Read RedeemMerchantDetails fields
byte hasRedeemDetails = source.readByte();
if (hasRedeemDetails == 1) {
this.redeemDetails = new RedeemMerchantDetails();
this.redeemDetails.setImgPreview(source.readString());
this.redeemDetails.setName(source.readString());
this.redeemDetails.setUuid(source.readString());
this.redeemDetails.setRedeemedDate(source.readString());
}
}
@Override
......@@ -355,6 +369,19 @@ public class Coupon implements Parcelable, Serializable {
dest.writeDouble(this.final_price);
dest.writeString(this.short_description);
dest.writeString(this.terms);
dest.writeString(this.changesDates != null ? this.changesDates.toString() : null);
dest.writeParcelable(this.couponsetDetails, flags);
dest.writeParcelable(this.merchantDetails, flags);
// Write RedeemMerchantDetails fields
if (this.redeemDetails != null) {
dest.writeByte((byte) 1);
dest.writeString(this.redeemDetails.getImgPreview());
dest.writeString(this.redeemDetails.getName());
dest.writeString(this.redeemDetails.getUuid());
dest.writeString(this.redeemDetails.getRedeemedDate());
} else {
dest.writeByte((byte) 0);
}
}
/**
......@@ -365,26 +392,26 @@ public class Coupon implements Parcelable, Serializable {
public JSONObject toJSONObject() {
JSONObject jObj = new JSONObject();
try {
jObj.putOpt(BARCODE, this.barcode);
jObj.putOpt(CATEGORY, this.category);
jObj.putOpt(COUPON, this.coupon);
jObj.putOpt(CREATED, this.created);
jObj.putOpt(DESCRIPTION, this.description);
jObj.putOpt(DISCOUNT, this.discount);
jObj.putOpt(EXPIRATION, this.expiration);
jObj.putOpt(IMAGE, this.image);
jObj.putOpt(NAME, this.name);
jObj.put(BARCODE, this.barcode != null ? this.barcode : JSONObject.NULL);
jObj.put(CATEGORY, this.category != null ? this.category : JSONObject.NULL);
jObj.put(COUPON, this.coupon != null ? this.coupon : JSONObject.NULL);
jObj.put(CREATED, this.created != null ? this.created : JSONObject.NULL);
jObj.put(DESCRIPTION, this.description != null ? this.description : JSONObject.NULL);
jObj.put(DISCOUNT, this.discount != null ? this.discount : JSONObject.NULL);
jObj.put(EXPIRATION, this.expiration != null ? this.expiration : JSONObject.NULL);
jObj.put(IMAGE, this.image != null ? this.image : JSONObject.NULL);
jObj.put(NAME, this.name != null ? this.name : JSONObject.NULL);
jObj.putOpt(STATUS, this.status);
jObj.putOpt(TRANSACTION_DATE, this.transactionDate);
jObj.putOpt(TRANSACTION_UUID, this.transactionUuid);
jObj.putOpt(CHANGES_DATES, this.changesDates);
jObj.putOpt(COUPONSET_UUID, this.couponsetUuid);
jObj.putOpt(MERCHANT_UUID, this.merchantUuid);
jObj.putOpt(INNER_TEXT, this.innerText);
jObj.putOpt(DISCOUNT_TYPE, this.discount_type);
jObj.put(TRANSACTION_DATE, this.transactionDate != null ? this.transactionDate : JSONObject.NULL);
jObj.put(TRANSACTION_UUID, this.transactionUuid != null ? this.transactionUuid : JSONObject.NULL);
jObj.put(CHANGES_DATES, this.changesDates != null ? this.changesDates : JSONObject.NULL);
jObj.put(COUPONSET_UUID, this.couponsetUuid != null ? this.couponsetUuid : JSONObject.NULL);
jObj.put(MERCHANT_UUID, this.merchantUuid != null ? this.merchantUuid : JSONObject.NULL);
jObj.put(INNER_TEXT, this.innerText != null ? this.innerText : JSONObject.NULL);
jObj.put(DISCOUNT_TYPE, this.discount_type != null ? this.discount_type : JSONObject.NULL);
jObj.putOpt(FINAL_PRICE, this.final_price);
jObj.putOpt(SHORT_DESCRIPTION, this.short_description);
jObj.putOpt(TERMS, this.terms);
jObj.put(SHORT_DESCRIPTION, this.short_description != null ? this.short_description : JSONObject.NULL);
jObj.put(TERMS, this.terms != null ? this.terms : JSONObject.NULL);
} catch (JSONException e) {
if (WarpConstants.DEBUG) {
e.printStackTrace();
......
......@@ -5,7 +5,6 @@ import java.util.concurrent.TimeUnit;
import ly.warp.sdk.Warply;
import ly.warp.sdk.utils.WarplyProperty;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
......@@ -49,11 +48,11 @@ public class ApiClient {
private static OkHttpClient getClient() {
/* Logs Enabled */
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
// interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addInterceptor(interceptor) // Logs Enabled
// .addInterceptor(interceptor) // Logs Enabled
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
......
......@@ -971,7 +971,7 @@ public class WarpUtils {
if (topView != null) {
topView.setPadding(
topView.getPaddingLeft(),
insets.top,
topView.getPaddingTop() + insets.top,
topView.getPaddingRight(),
topView.getPaddingBottom()
);
......@@ -983,7 +983,7 @@ public class WarpUtils {
bottomView.getPaddingLeft(),
bottomView.getPaddingTop(),
bottomView.getPaddingRight(),
insets.bottom
bottomView.getPaddingBottom() + insets.bottom
);
}
......
......@@ -35,6 +35,7 @@ import ly.warp.sdk.Warply;
import ly.warp.sdk.db.WarplyDBHelper;
import ly.warp.sdk.io.models.BannerItem;
import ly.warp.sdk.io.models.Campaign;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.io.models.CouponList;
import ly.warp.sdk.io.models.Couponset;
import ly.warp.sdk.utils.managers.WarplyManager;
......@@ -54,6 +55,7 @@ public class WarplyManagerHelper {
// ===========================================================
private static CouponList mCouponRedeemedList = new CouponList();
private static ArrayList<Coupon> mCouponList = new ArrayList<Coupon>();
private static ArrayList<Campaign> mCampaignListAll = new ArrayList<Campaign>();
private static ArrayList<BannerItem> mBannerListAll = new ArrayList<BannerItem>();
private static LinkedHashMap<String, ArrayList<Couponset>> mCouponsetCategorizedMap = new LinkedHashMap<>();
......@@ -98,6 +100,15 @@ public class WarplyManagerHelper {
mCouponRedeemedList.addAll(couponRedeemedList);
}
public static void setCoupons(ArrayList<Coupon> couponList) {
mCouponList.clear();
mCouponList.addAll(couponList);
}
public static ArrayList<Coupon> getCoupons() {
return mCouponList;
}
public static String constructCampaignUrl(Campaign item) {
WarplyManager.getSingleCampaign(item.getSessionUUID());
String url = item.getIndexUrl();
......
......@@ -49,7 +49,6 @@ public class WarplyProperty {
public static final String KEY_LOGIN_TYPE = "LoginType";
public static final String KEY_DL_URL_SCHEME = "DL_URL_SCHEME";
public static final String KEY_BASE_URL = "BaseURL";
public static final String KEY_VERIFY_URL = "VerifyURL";
// ===========================================================
// Methods
......@@ -204,10 +203,6 @@ public class WarplyProperty {
return getWarplyProperty(context, KEY_BASE_URL);
}
public static String getVerifyUrl(Context context) {
return getWarplyProperty(context, KEY_VERIFY_URL);
}
public static boolean isSendPackages(Context context) {
return Boolean.parseBoolean(getWarplyProperty(context, KEY_SEND_PACKAGES));
}
......
......@@ -61,7 +61,6 @@ import ly.warp.sdk.io.models.BannerItem;
import ly.warp.sdk.io.models.Campaign;
import ly.warp.sdk.io.models.Content;
import ly.warp.sdk.io.models.Coupon;
import ly.warp.sdk.io.models.CouponList;
import ly.warp.sdk.io.models.Couponset;
import ly.warp.sdk.io.models.Merchant;
import ly.warp.sdk.io.models.User;
......@@ -371,7 +370,7 @@ public class WarplyManager {
});
}
public static void getUserCouponsWithCouponsets(final CallbackReceiver<CouponList> receiver) {
public static void getCoupons(final CallbackReceiver<ArrayList<Coupon>> receiver) {
WarpUtils.log("************* WARPLY User Coupons Request ********************");
WarpUtils.log("[WARP Trace] WARPLY User Coupons Request is active");
WarpUtils.log("**************************************************");
......@@ -379,18 +378,18 @@ public class WarplyManager {
ApiService service = ApiClient.getRetrofitInstance().create(ApiService.class);
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
SettableFuture<CouponList> futureUniversal = SettableFuture.create();
ListenableFuture<CouponList> futureCoupons = getCouponsUniversalRetro(service, 0, futureUniversal);
SettableFuture<ArrayList<Coupon>> futureUniversal = SettableFuture.create();
ListenableFuture<ArrayList<Coupon>> futureCoupons = getCouponsUniversalRetro(service, 0, futureUniversal);
ListenableFuture<List<Object>> allResultsFuture = Futures.allAsList(futureCoupons);
ListenableFuture<CouponList> mergedResultFuture = Futures.transformAsync(allResultsFuture, results -> {
CouponList resultCoupons = (CouponList) results.get(0);
ListenableFuture<ArrayList<Coupon>> mergedResultFuture = Futures.transformAsync(allResultsFuture, results -> {
ArrayList<Coupon> resultCoupons = (ArrayList<Coupon>) results.get(0);
return executorService.submit(() -> resultCoupons);
}, executorService);
Futures.addCallback(mergedResultFuture, new FutureCallback<CouponList>() {
Futures.addCallback(mergedResultFuture, new FutureCallback<ArrayList<Coupon>>() {
@Override
public void onSuccess(CouponList mergedResult) {
public void onSuccess(ArrayList<Coupon> mergedResult) {
executorService.shutdownNow();
new Handler(Looper.getMainLooper()).post(() -> receiver.onSuccess(mergedResult));
}
......@@ -463,7 +462,7 @@ public class WarplyManager {
ApiService service = ApiClient.getRetrofitInstance().create(ApiService.class);
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(2));
ListenableFuture<ArrayList<Couponset>> futureCouponsets = getCouponsetsRetro(service);
ListenableFuture<ArrayList<Couponset>> futureCouponsets = getCouponsetsRetro(service, 0);
ListenableFuture<ArrayList<Merchant>> futureMerchants = getMerchantsRetro(service);
ListenableFuture<List<Object>> allResultsFuture = Futures.allAsList(futureCouponsets, futureMerchants);
......@@ -488,7 +487,7 @@ public class WarplyManager {
}, executorService);
}
private static ListenableFuture<ArrayList<Couponset>> getCouponsetsRetro(ApiService service/*, Callback<ResponseBody> callback*/) {
private static ListenableFuture<ArrayList<Couponset>> getCouponsetsRetro(ApiService service, int tries/*, Callback<ResponseBody> callback*/) {
SettableFuture<ArrayList<Couponset>> future = SettableFuture.create();
String timeStamp = DateFormat.format("yyyy-MM-dd hh:mm:ss", System.currentTimeMillis()).toString();
......@@ -550,6 +549,31 @@ public class WarplyManager {
} else {
future.set(new ArrayList<Couponset>());
}
} else if (response.code() == 401) {
refreshToken(new WarplyRefreshTokenRequest(), new CallbackReceiver<JSONObject>() {
@Override
public void onSuccess(JSONObject result) {
int status = result.optInt("status", 2);
if (status == 1)
getCouponsetsRetro(service, tries);
else {
if (tries < MAX_RETRIES) {
getCouponsetsRetro(service, (tries + 1));
} else {
future.setException(new Throwable());
}
}
}
@Override
public void onFailure(int errorCode) {
if (tries < MAX_RETRIES) {
getCouponsetsRetro(service, (tries + 1));
} else {
future.setException(new Throwable());
}
}
});
} else if (String.valueOf(response.code()).startsWith("5")) {
future.set(new ArrayList<Couponset>());
} else {
......@@ -985,7 +1009,7 @@ public class WarplyManager {
return future;
}
private static ListenableFuture<CouponList> getCouponsUniversalRetro(ApiService service, int tries, SettableFuture<CouponList> future) {
private static ListenableFuture<ArrayList<Coupon>> getCouponsUniversalRetro(ApiService service, int tries, SettableFuture<ArrayList<Coupon>> future) {
String timeStamp = DateFormat.format("yyyy-MM-dd hh:mm:ss", System.currentTimeMillis()).toString();
String apiKey = WarpUtils.getApiKey(Warply.getWarplyContext());
String webId = WarpUtils.getWebId(Warply.getWarplyContext());
......@@ -995,17 +1019,8 @@ public class WarplyManager {
jsonParams.put("action", "user_coupons");
JSONArray jArr = new JSONArray();
jArr.put("merchant");
jArr.put("redemption");
jsonParams.put("details", jArr);
jsonParams.put("language", WarpUtils.getApplicationLocale(Warply.getWarplyContext()));
// JSONObject jPagination= new JSONObject();
// try {
// jPagination.putOpt("page",1);
// jPagination.putOpt("per_page", 10);
// } catch (JSONException e) {
// throw new RuntimeException(e);
// }
// jsonParams.put("pagination", jPagination);
// jsonParams.put("language", WarpUtils.getApplicationLocale(Warply.getWarplyContext()));
jsonParamsCoupons.put("coupon", jsonParams);
RequestBody couponsRequest = RequestBody.create(MediaType.get("application/json; charset=utf-8"), (new JSONObject(jsonParamsCoupons)).toString());
......@@ -1032,32 +1047,29 @@ public class WarplyManager {
}
if (jCouponsBody != null) {
CouponList mCouponRedeemedList = new CouponList();
ArrayList<Coupon> mCouponList = new ArrayList<Coupon>();
final ExecutorService executorCoupons = Executors.newFixedThreadPool(1);
JSONArray finalJCouponsBody = jCouponsBody;
executorCoupons.submit(() -> {
for (int i = 0; i < finalJCouponsBody.length(); ++i) {
Coupon tempCoupon = new Coupon(finalJCouponsBody.optJSONObject(i), true);
if (tempCoupon.getStatus() == 0) {
mCouponRedeemedList.add(tempCoupon);
}
mCouponList.add(tempCoupon);
}
WarplyManagerHelper.setCouponRedeemedList(mCouponRedeemedList);
WarplyManagerHelper.setCoupons(mCouponList);
Collections.sort(mCouponRedeemedList, (coupon1, coupon2) -> coupon1.getExpirationDate().compareTo(coupon2.getExpirationDate()));
Collections.sort(mCouponList, (coupon1, coupon2) -> coupon1.getExpirationDate().compareTo(coupon2.getExpirationDate()));
executorCoupons.shutdownNow();
future.set(mCouponRedeemedList);
future.set(mCouponList);
});
} else {
future.set(new CouponList());
future.set(new ArrayList<Coupon>());
}
} else {
future.set(new CouponList());
future.set(new ArrayList<Coupon>());
}
} else if (response.code() == 401) {
refreshToken(new WarplyRefreshTokenRequest(), new CallbackReceiver<JSONObject>() {
......@@ -1071,7 +1083,7 @@ public class WarplyManager {
if (tries < MAX_RETRIES) {
getCouponsUniversalRetro(service, (tries + 1), future);
} else {
// future.set(new CouponList());
// future.set(new ArrayList<Coupon>());
future.setException(new Throwable());
}
}
......@@ -1083,20 +1095,20 @@ public class WarplyManager {
if (tries < MAX_RETRIES) {
getCouponsUniversalRetro(service, (tries + 1), future);
} else {
// future.set(new CouponList());
// future.set(new ArrayList<Coupon>());
future.setException(new Throwable());
}
}
});
} else {
// future.set(new CouponList());
// future.set(new ArrayList<Coupon>());
future.setException(new Throwable());
}
}
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
// future.set(new CouponList());
// future.set(new ArrayList<Coupon>());
future.setException(new Throwable());
}
});
......
......@@ -25,7 +25,8 @@
android:background="@color/custom_grey_light"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp">
android:paddingBottom="16dp"
android:paddingTop="16dp">
<LinearLayout
android:layout_width="0dp"
......@@ -104,12 +105,19 @@
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/ll_sections_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:paddingBottom="16dp"
android:orientation="vertical" />
<RelativeLayout
android:id="@+id/rl_sections_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_marginTop="48dp"
android:translationZ="100dp"
android:visibility="gone"
tools:visibility="visible">
......@@ -122,13 +130,6 @@
android:indeterminateTint="@color/custom_light_blue"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
<LinearLayout
android:id="@+id/ll_sections_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="48dp"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>
......
......@@ -223,6 +223,25 @@
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingHorizontal="16dp" />
<RelativeLayout
android:id="@+id/pb_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_marginTop="64dp"
android:translationZ="100dp"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerInParent="true"
android:indeterminate="true"
android:indeterminateTint="@color/custom_light_blue"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
......
......@@ -59,8 +59,7 @@
android:id="@+id/iv_coupon_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
tools:src="@drawable/demo_home_banner1"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
......@@ -280,7 +279,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/demo_qr_code"
android:text="@string/demo_barcode_code"
android:textColor="@color/custom_black2"
android:textSize="15sp" />
......@@ -303,10 +302,10 @@
<ImageView
android:id="@+id/iv_qr_code"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_height="120dp"
android:layout_marginVertical="16dp"
android:scaleType="fitCenter"
android:src="@drawable/demo_qr" />
android:src="@drawable/demo_barcode" />
</LinearLayout>
</LinearLayout>
......
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/custom_grey_light">
<ScrollView
android:id="@+id/coupon_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:id="@+id/home_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_back"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_back" />
<TextView
android:id="@+id/tv_header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/demo_offer"
android:textColor="@color/custom_black4"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="210dp">
<ImageView
android:id="@+id/iv_coupon_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/demo_shape_white_border_grey"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingVertical="3dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginEnd="4dp"
android:src="@drawable/ic_info" />
<TextView
android:id="@+id/tv_more_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/demo_more"
android:textColor="@color/custom_black4"
android:textSize="11sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/ll_coupon_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp"
android:layout_marginBottom="48dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/ll_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ll_buttons"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/tv_coupon_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/custom_pink2"
android:textSize="23sp"
tools:text="@string/demo_more" />
<TextView
android:id="@+id/tv_coupon_small_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:text="@string/demo_purchases"
android:textColor="@color/custom_black5"
android:textSize="17sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:src="@drawable/demo_heart_border" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/demo_folder" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_coupon_end_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:includeFontPadding="false"
android:textColor="@color/custom_black5"
android:textSize="13sp"
tools:text="@string/demo_purchases" />
<TextView
android:id="@+id/tv_coupon_full_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:lineSpacingExtra="2dp"
android:maxLines="4"
android:textColor="@color/custom_black5"
android:textSize="17sp"
tools:text="Πάρτε τα πακέτα κινητής στη μισή τιμή μόνο αυτό το μήνα. Απεριόριστα λεπτά προς όλα τα δίκτυα, SMS και 50GB δεδομένα υψηλής ταχύτητας. Ισχύει για νέους συνδρομητές και ανανεώσεις συμβολαίων. Δωρεάν ενεργοποίηση και τεχνική υποστήριξη." />
<TextView
android:id="@+id/tv_more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="2dp"
android:text="@string/demo_more"
android:textColor="@color/custom_skyblue"
android:textSize="15sp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/terms_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:orientation="vertical"
android:paddingHorizontal="2dp">
<LinearLayout
android:id="@+id/terms_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_terms_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/demo_terms"
android:textColor="@color/custom_black2"
android:textSize="15sp"/>
<ImageView
android:id="@+id/iv_terms_arrow"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:src="@drawable/ic_arrow_down" />
</LinearLayout>
<LinearLayout
android:id="@+id/terms_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/tv_terms_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:lineSpacingExtra="4dp"
android:text="@string/demo_lorem_ipsum"
android:textColor="@color/custom_black2"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="48dp"
android:background="@drawable/selector_button_black"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_shops_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/demo_shops"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="20dp"
android:background="@drawable/selector_button_transparent_black_border"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_website_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/demo_website"
android:textColor="@color/custom_black2"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</RelativeLayout>
......@@ -87,7 +87,6 @@
app:layout_constraintBottom_toBottomOf="parent"
tools:text="έως 30-09" />
<!-- Brand Logo -->
<ImageView
android:id="@+id/iv_logo"
android:layout_width="80dp"
......
......@@ -16,7 +16,8 @@
android:layout_height="wrap_content"
android:background="@android:color/white"
android:paddingHorizontal="16dp"
android:paddingVertical="16dp">
android:paddingBottom="16dp"
android:paddingTop="16dp">
<ImageView
android:id="@+id/user_img"
......
......@@ -15,6 +15,7 @@
<string name="demo_valid_until">Η προσφορά ισχύει έως %1$s</string>
<string name="demo_coupon_code">Κωδικός Κουπονιού</string>
<string name="demo_qr_code">QR Κουπονιού</string>
<string name="demo_barcode_code">Barcode Κουπονιού</string>
<string name="demo_terms">Όροι Χρήσης</string>
<string name="demo_copy_success">Ο κωδικός αντιγράφηκε στο πρόχειρο</string>
<string name="demo_lorem_ipsum">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</string>
......