Skip to content

Commit 519e193

Browse files
committed
Add helper class to perform billing operations
Fix subscription translations.
1 parent 96c9054 commit 519e193

File tree

4 files changed

+306
-3
lines changed

4 files changed

+306
-3
lines changed

dynamic-billing/src/main/java/com/pranavpandey/android/dynamic/billing/model/DynamicProduct.java

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Pranav Pandey
2+
* Copyright 2022-2025 Pranav Pandey
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@
2323

2424
import com.android.billingclient.api.BillingClient;
2525

26+
import java.util.regex.Pattern;
27+
2628
/**
2729
* A class to represent the in-app product or subscription.
2830
*/
@@ -38,6 +40,48 @@ public class DynamicProduct implements Parcelable {
3840
*/
3941
private final @BillingClient.ProductType String type;
4042

43+
/**
44+
* Interface to hold the period constants.
45+
*/
46+
public @interface Period {
47+
48+
/**
49+
* Constant for the daily period.
50+
*/
51+
String DAY = "D";
52+
53+
/**
54+
* Constant for the weekly period.
55+
*/
56+
String WEEK = "W";
57+
58+
/**
59+
* Constant for the monthly period.
60+
*/
61+
String MONTH = "M";
62+
63+
/**
64+
* Constant for the yearly period.
65+
*/
66+
String YEAR = "Y";
67+
}
68+
69+
/**
70+
* Interface to hold the pricing and period patterns.
71+
*/
72+
public @interface Patterns {
73+
74+
/**
75+
* Pattern to match a digit.
76+
*/
77+
Pattern DIGIT = Pattern.compile(".*\\d.*");
78+
79+
/**
80+
* Pattern to match a number.
81+
*/
82+
Pattern NUMBER = Pattern.compile("[^0-9]");
83+
}
84+
4185
/**
4286
* Constructor to initialize an object of this class.
4387
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* Copyright 2022-2025 Pranav Pandey
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.pranavpandey.android.dynamic.billing.util;
18+
19+
import android.content.Context;
20+
21+
import androidx.annotation.Nullable;
22+
23+
import com.android.billingclient.api.ProductDetails;
24+
import com.pranavpandey.android.dynamic.billing.R;
25+
import com.pranavpandey.android.dynamic.billing.model.DynamicProduct;
26+
27+
import java.util.List;
28+
29+
/**
30+
* Helper class to perform billing related operations.
31+
*/
32+
public class DynamicBillingUtils {
33+
34+
/**
35+
* Returns the user friendly string to display the trial information.
36+
*
37+
* @param context The context to retrieve resources.
38+
* @param period The ISO 8601 formatted pricing phase period.
39+
*
40+
* @return The user friendly string to display the trial information.
41+
*/
42+
public static @Nullable String getStringForFreeTrial(
43+
@Nullable Context context, @Nullable String period) {
44+
if (context == null || period == null) {
45+
return null;
46+
}
47+
48+
int periodCount = Integer.parseInt(period.replaceAll(
49+
DynamicProduct.Patterns.NUMBER.pattern(), ""));
50+
51+
if (period.contains(DynamicProduct.Period.DAY)) {
52+
return String.format(context.getString(
53+
R.string.adb_offer_free_trial_day), periodCount);
54+
} else if (period.contains(DynamicProduct.Period.WEEK)) {
55+
return String.format(context.getString(
56+
R.string.adb_offer_free_trial_week), periodCount);
57+
} else if (period.contains(DynamicProduct.Period.MONTH)) {
58+
return String.format(context.getString(
59+
R.string.adb_offer_free_trial_month), periodCount);
60+
} else if (period.contains(DynamicProduct.Period.YEAR)) {
61+
return String.format(context.getString(
62+
R.string.adb_offer_free_trial_year), periodCount);
63+
}
64+
65+
return period;
66+
}
67+
68+
/**
69+
* Returns the user friendly string to display the first cycle information.
70+
*
71+
* @param context The context to retrieve resources.
72+
* @param formattedPrice The formatted price along with the currency symbol.
73+
* @param period The ISO 8601 formatted pricing phase period.
74+
*
75+
* @return The user friendly string to display the first cycle information.
76+
*/
77+
public static @Nullable String getStringForFirstCycle(@Nullable Context context,
78+
@Nullable String formattedPrice, @Nullable String period) {
79+
if (context == null || period == null) {
80+
return null;
81+
}
82+
83+
int periodCount = Integer.parseInt(period.replaceAll(
84+
DynamicProduct.Patterns.NUMBER.pattern(), ""));
85+
String formattedPeriod = period;
86+
87+
if (period.contains(DynamicProduct.Period.DAY)) {
88+
formattedPeriod = periodCount > 1 ? String.format(
89+
context.getString(R.string.adb_offer_first_days), periodCount)
90+
: context.getString(R.string.adb_offer_first_day);
91+
} else if (period.contains(DynamicProduct.Period.WEEK)) {
92+
formattedPeriod = periodCount > 1 ? String.format(
93+
context.getString(R.string.adb_offer_first_weeks), periodCount)
94+
: context.getString(R.string.adb_offer_first_week);
95+
} else if (period.contains(DynamicProduct.Period.MONTH)) {
96+
formattedPeriod = periodCount > 1 ? String.format(
97+
context.getString(R.string.adb_offer_first_months), periodCount)
98+
: context.getString(R.string.adb_offer_first_month);
99+
} else if (period.contains(DynamicProduct.Period.YEAR)) {
100+
formattedPeriod = periodCount > 1 ? String.format(
101+
context.getString(R.string.adb_offer_first_years), periodCount)
102+
: context.getString(R.string.adb_offer_first_year);
103+
}
104+
105+
return String.format(context.getString(R.string.ads_format_blank_space),
106+
formattedPrice, formattedPeriod);
107+
}
108+
109+
/**
110+
* Returns the user friendly string to display the base cycle information.
111+
*
112+
* @param context The context to retrieve resources.
113+
* @param formattedPrice The formatted price along with the currency symbol.
114+
* @param period The ISO 8601 formatted pricing phase period.
115+
*
116+
* @return The user friendly string to display the base cycle information.
117+
*/
118+
public static @Nullable String getStringForBaseCycle(@Nullable Context context,
119+
@Nullable String formattedPrice, @Nullable String period) {
120+
if (context == null || period == null) {
121+
return null;
122+
}
123+
124+
int periodCount = Integer.parseInt(period.replaceAll(
125+
DynamicProduct.Patterns.NUMBER.pattern(), ""));
126+
String formattedPeriod = period;
127+
128+
if (period.contains(DynamicProduct.Period.DAY)) {
129+
formattedPeriod = periodCount > 1 ? String.format(
130+
context.getString(R.string.adb_price_days), periodCount)
131+
: context.getString(R.string.adb_price_day);
132+
} else if (period.contains(DynamicProduct.Period.WEEK)) {
133+
formattedPeriod = periodCount > 1 ? String.format(
134+
context.getString(R.string.adb_price_weeks), periodCount)
135+
: context.getString(R.string.adb_price_week);
136+
} else if (period.contains(DynamicProduct.Period.MONTH)) {
137+
formattedPeriod = periodCount > 1 ? String.format(
138+
context.getString(R.string.adb_price_months), periodCount)
139+
: context.getString(R.string.adb_price_month);
140+
} else if (period.contains(DynamicProduct.Period.YEAR)) {
141+
formattedPeriod = periodCount > 1 ? String.format(
142+
context.getString(R.string.adb_price_years), periodCount)
143+
: context.getString(R.string.adb_price_year);
144+
}
145+
146+
return String.format(context.getString(R.string.ads_format_blank_space),
147+
formattedPrice, formattedPeriod);
148+
}
149+
150+
/**
151+
* Returns a string based on the formatted price, period and cycle count.
152+
*
153+
* @param context The context to retrieve resources.
154+
* @param formattedPrice The formatted price along with the currency symbol.
155+
* @param period The ISO 8601 formatted pricing phase period.
156+
* @param cycleCount The period cycle count.
157+
*
158+
* @return The string based on the formatted price, period and cycle count.
159+
*/
160+
public static @Nullable String getStringForPeriod(@Nullable Context context,
161+
@Nullable String formattedPrice, @Nullable String period, int cycleCount) {
162+
if (context == null || formattedPrice == null || period == null) {
163+
return null;
164+
}
165+
166+
if (DynamicProduct.Patterns.DIGIT.matcher(formattedPrice).matches()) {
167+
if (cycleCount == 1) {
168+
return getStringForFirstCycle(
169+
context, formattedPrice, period);
170+
} else {
171+
return getStringForBaseCycle(
172+
context, formattedPrice, period);
173+
}
174+
} else {
175+
return getStringForFreeTrial(context, period);
176+
}
177+
}
178+
179+
/**
180+
* Returns a string based on the offer pricing phase(s).
181+
*
182+
* @param context The context to retrieve resources.
183+
* @param pricingPhases The pricing phases.
184+
* @param withBase {@code true} to include base pricing phase.
185+
*
186+
* @return A string based on the offer pricing phase(s).
187+
*
188+
* @see #getStringForPeriod(Context, String, String, int)
189+
*/
190+
public static @Nullable String getPricingPhasesDetails(@Nullable Context context,
191+
@Nullable List<ProductDetails.PricingPhase> pricingPhases, boolean withBase) {
192+
if (context == null || pricingPhases == null) {
193+
return null;
194+
}
195+
196+
StringBuilder offerDetailsBuilder = new StringBuilder();
197+
for (ProductDetails.PricingPhase offerPricingPhase : pricingPhases) {
198+
if (!withBase && offerPricingPhase.getBillingCycleCount() <= 0) {
199+
continue;
200+
}
201+
202+
String pricingPhaseInfo = getStringForPeriod(context,
203+
offerPricingPhase.getFormattedPrice(), offerPricingPhase.getBillingPeriod(),
204+
offerPricingPhase.getBillingCycleCount());
205+
206+
if (offerDetailsBuilder.length() > 0) {
207+
offerDetailsBuilder.replace(0, offerDetailsBuilder.length(),
208+
String.format(context.getString(R.string.ads_format_next_line),
209+
offerDetailsBuilder, pricingPhaseInfo));
210+
} else {
211+
offerDetailsBuilder.append(pricingPhaseInfo);
212+
}
213+
}
214+
215+
return offerDetailsBuilder.toString();
216+
}
217+
218+
/**
219+
* Returns a string based on the offer pricing phase(s).
220+
*
221+
* @param context The context to retrieve resources.
222+
* @param offer The offer to retrieve the pricing phases.
223+
* @param withBase {@code true} to include base pricing phase.
224+
*
225+
* @return A string based on the offer pricing phase(s).
226+
*
227+
* @see #getPricingPhasesDetails(Context, List, boolean)
228+
*/
229+
public static @Nullable String getOfferDetails(@Nullable Context context,
230+
@Nullable ProductDetails.SubscriptionOfferDetails offer, boolean withBase) {
231+
if (context == null || offer == null) {
232+
return null;
233+
}
234+
235+
return getPricingPhasesDetails(context,
236+
offer.getPricingPhases().getPricingPhaseList(), withBase);
237+
}
238+
239+
/**
240+
* Returns a string based on the base offer pricing.
241+
*
242+
* @param context The context to retrieve resources.
243+
* @param offer The offer to retrieve the pricing phases.
244+
*
245+
* @return A string based on the base offer pricing.
246+
*
247+
* @see #getPricingPhasesDetails(Context, List, boolean)
248+
*/
249+
public static @Nullable String getOfferDetailsBase(@Nullable Context context,
250+
@Nullable ProductDetails.SubscriptionOfferDetails offer) {
251+
if (context == null || offer == null) {
252+
return null;
253+
}
254+
255+
int size = offer.getPricingPhases().getPricingPhaseList().size();
256+
return getPricingPhasesDetails(context, offer.getPricingPhases()
257+
.getPricingPhaseList().subList(size - 1, size), true);
258+
}
259+
}

dynamic-billing/src/main/res/values-de/strings.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
<string name="adb_price_month">pro Monat</string>
5353
<string name="adb_price_months">pro %1$d Monate</string>
5454
<string name="adb_price_year">pro Jahr</string>
55-
<string name="adb_price_years">pro Jahr</string>
55+
<string name="adb_price_years">pro %1$d Jahre</string>
5656

5757
<!-- Offers -->
5858
<string name="adb_offer_free_trial_day">%1$d&#8211;tägige kostenlose Testversion</string>

dynamic-billing/src/main/res/values-in/strings.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
<string name="adb_offer_first_week">untuk minggu pertama</string>
6565
<string name="adb_offer_first_weeks">untuk %1$d minggu pertama</string>
6666
<string name="adb_offer_first_month">untuk bulan pertama</string>
67-
<string name="adb_offer_first_months">untuk bulan pertama</string>
67+
<string name="adb_offer_first_months">untuk %1$d bulan pertama</string>
6868
<string name="adb_offer_first_year">untuk tahun pertama</string>
6969
<string name="adb_offer_first_years">untuk %1$d tahun pertama</string>
7070

0 commit comments

Comments
 (0)