diff --git a/android/app/build.gradle b/android/app/build.gradle
index 35efa81..43a6c8c 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -78,5 +78,7 @@ dependencies {
implementation 'com.google.firebase:firebase-analytics:18.0.0'
implementation 'com.facebook.android:facebook-login:5.15.3'
implementation 'com.android.support:multidex:1.0.3'
+ def billing_version = "3.0.0"
+ implementation "com.android.billingclient:billing:$billing_version"
}
sourceCompatibility = '1.8'
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 7f52f63..a80fc66 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -50,12 +50,12 @@
android:value="2" />
+ android:value="@string/facebook_app_id"/>
+ android:label="@string/app_name" />
diff --git a/asset/data/promotion_codes-month.csv b/asset/data/promotion_codes-month.csv
new file mode 100644
index 0000000..86abb63
--- /dev/null
+++ b/asset/data/promotion_codes-month.csv
@@ -0,0 +1,200 @@
+Promotion code
+ - A7DXXDNMKXU0KUJAEW70PRF
+2HLKJ93QP0TTGYFB4EL383L
+VDZBBGVMUUZ9PF2HTPG7PJ8
+V7RF0U3H6T0CY5LXX6AH7A4
+D63MQ1JPW6BQ4P0ZQM9UNQV
+ZDT7SSQB01FG4LCE67APADL
+U6953FSGRD54HYSAP27BQSA
+P0EHCGYHBM8PCVDY3AETGBY
+RUQX8TPKLT83WZ102EQ18B6
+LZ7MD5N30VQ6RD13J7NXHLL
+02FR52AH4WSANVMFGNQLBHX
+NLT4166PUJYW6ARAX76VK6Z
+TT6J3EBD56YZEK5T7R48ZB7
+PJ9APMP07V716P0MZQ1QQZ4
+1LAEA5MAVZCQ6Q6V746VWHA
+UR80FUSC88S4ZM6D7F2LAHX
+QGSTE9Z6T4ZTBR7D18ENZZE
+42F6RJ8UC0JGTJ3J3SPAW51
+Y1Z3S9D375EW5QG0KDWHRN5
+5KSKAP7JMSZB8KFWQCNJ2EP
+RFPKVGG2HUTLJDS293SK7T5
+6AH8L8V6ZDXRFDGWLD7XC1R
+L34MAXPGWBPHHEDRJ6J56Y2
+ZHD36F0FSPQ0302UG9JG4SJ
+A534UX0580PET9ZLRJGSJBN
+H8DZKTCYBXX3Y03UGQFVFFL
+2EN7JH4A3KCJB2H49WBT4NX
+XKP4HLPL738ZWAHH0DFPV73
+QA980ELAHDVJKAAALCSX3EC
+RRXA7TKYR8T0GRUS7QEBMU6
+3E9K4YZ4JG6UM9A03LQYX8Z
+YTYYQE4B1HNCAPVEWHAHBYN
+TAUZRSDGB8HT4JHWT04R0T0
+2APA0FT1DH0XM35PESM7YX0
+CVLTAJM0NF1GX46FEYCKXR1
+A8GH58V40TSC6L86ECWRNBT
+QJ37ENK3TK3FTZA250DGS02
+TJJWKUMBQRPTT8VWZDF4W9X
+CM6DDDUYVCGSGXZ36D5N6WW
+AZMUZ9EPL0JBHYKGRK3MCK0
+12MV9LC4BE4KGEDEW5X2MEM
+41F0GRVWJSLEAUAY0ALUN1V
+PPPHG266FFF6QBGJSL8WBDF
+MGP71PUFZ6NKLPP4VF8DS3T
+LXA6SH75WZ8KY4BEUV0S85U
+447FRJGYW2902PLLXZYTRAU
+D9EMMELU70PXZ8AKUZ56WCZ
+CZ4VPRSD6RFCGRDM1SVU2BH
+1ZTL14ZD7GHVB6B0NQMTCX5
+SA5W6ABBE74VUJV077C9A7E
+WB9EMATS4DHY3N2QL8WPXGB
+XQ368K414VJVVAS4G0EBT7C
+KGXV8WTQZ39WUUFYUJQXS1Y
+KW9920H2TDVJG9M6WVH2MBZ
+EZUSXBLAT69K7MBYUULPSUY
+3ZW2R3HFQB0GT27253L530Q
+WNWUYXYZZLVT5EK5NH4UMB7
+7WSV8KB1DH5XEBE9ZBTVSVF
+0Q3USEXJDRN219V66275B4H
+U6LKPXG16Z83A1RA320P0U6
+PAV3UQXD61NR528X1GND42R
+6NNZ9MVWZESFPHV0VBQBMNN
+BD2XLH4HKJWCGPY9C1AJGNW
+RQTLUQ1ZZJZBET76071WT89
+RBKQRGP9E6QGUX3KAJXF20K
+UGM5QUE0EXKEBE2A75ECNSM
+N2P0H1LFS2YEKX64D2PWDN7
+P6VJLEAKABRGFVR4T5DH1X5
+TJ815RNL5A460XJG4THR9JL
+YL0XY8R2TBTP5N5F9S3VWSV
+DU32C5H5U4GLT0509DS9G2K
+9DXV8W7QU9L63EY7XJVYTQ7
+UXNLD05CKKX20YKKNLVJ4BS
+VYDMRWTW5FJ76XGLSML71QU
+MBJJHEJPTRY2HQXLLPJ51XR
+XM63E19C75AJUBHH98D7W6C
+PTAU2W57H36U9U547UUND9G
+F1MZVKQ0Q4186X2JAPKW4RP
+MJRP26W2QM6XKUJDKFXX6AY
+AFN2GK30M9XNAPCF6GA1PZY
+GMYZDKLK428WS94GYHSW97U
+WVJHNTJE2USGWYAUB2G2ZNK
+DPYWSJETW05X990Z0U2MWBS
+S8U1061Q6LRZNGWHTGJ04U0
+HTLC06BVKKBKS7SBVK26J45
+A91PFKAHJMMXFTAHRUB9ENK
+KXPJ0AV2QURS3T9Z3HR0RN8
+V24WFCGMMQ1X38EUXXAVCGW
+263NEVRL247Q3D82PU5SFDK
+Z4QPPMN3J2T12VTGFVEZ2K7
+29F4W2RCAWRMK9P7KWRQ0WK
+LHJEC87AM9TRC0C0JX3XLGX
+PFSML2E11VJEB9GJ9SR1T36
+ZECQPLLUVX3QR61WMUX42HA
+K8MJAX3THB3FCWZT9VB2GSV
+HV4NHY2E3YJ836HH5KKU9Z1
+H6VN3YCXJYL5KR63QNGUX1C
+MDLNPC7H171QQN2C4SU0MZ7
+48HAF1RBHFWC2H8ZUHU8MGG
+48Z1MS05VFAB5ZKDEYB17TA
+V5LGP8113UJJ1SEWC5B5GTE
+RR5FP5CY1JB2WB9KY0YRKJR
+AYGQQN8FFPGY557PG6VGTTK
+PXT14ECUUR2MYP7XH9LYUQ6
+FED9VS405XMWYHW2P14NGJN
+V5NBC31P2B1A9D536ERVBYH
+AUHL02U2UDZY9N25Z0QE9C8
+JLSRMKPW82XY5EALCPT1P7G
+JM2BQUT9ZM3DNMDLVPATZGP
+C26KUVKFHL0VJ03H95G6F2Q
+LBLDHBEA63LH8S97SBLLA2C
+GYNB2CCAU6QLGW79N2UJ4UT
+1KPWT2JNULPBE3B9H8FC8NL
+EPDC7FRE705861ZMRJ71BV4
+9E0CDJTDCG5HD7C2LWPFDS7
+D72UZ0B4EUBZE8C9ZNX7XVP
+W2S98ZA8BW5NJ7RKHBSEE7P
+HMCV6KNUVY3Q4WGM3ZVRKNY
+U0BQEC40RZ5CDLQH26PP4YT
+MTLLSJJN4LQVRS7JFL2Y2K6
+Z0G8BJ8NS9U8HYBHN3N99AE
+9J14SSRZAMZ31DG1ZXNVNNV
+WPS91QFT25LT7YXVYPG75BP
+G3A4HNEVT5CH077FWZZUK88
+QDKUWSRJR5K6Z97C4ZUGTWH
+5YZTDJ1J85QXZH6TXX3TDDQ
+ZM500MPBGWU0F2VHQQUKZ5T
+TTF0R47WTKWPJT4LATUKWHQ
+JS5A5F0RCZRVB971F2LE2HS
+1D8A1XVA3SKFCFWRSXQYRFV
+K3LE2X4GAL1MYPR9RY9EN6G
+24X5VZDJD3YZUABYW6YSVSC
+9C3R9XDFL24D7CTZMFH2CMN
+VX4B1HLX8VFZS2MPNN094TZ
+4Z7EKRK9YC3T130V0QWMKWL
+8AXQF15N9VESKNQRVXMGG87
+79T0R5HWKK91LUKJ0PMNDCB
+YG5ZGFAAWC9ZD8SM1HARLBL
+BMD30152E4L9WUL0Q5V7AF3
+R40VFDKQH0SVB3DJK170ZAZ
+D74L2A1KJED72TZ3DWVH8ZP
+0WT613SN6Y4K3J2G77D0EUT
+W7EH5EV6KZ5T8TA0DRSYCED
+U8HDE2HVB09ZB9TN4KAEAPR
+JV75U798X715X36PN2PE0D3
+UUD5YAN2J2UXG15LBHHCP7G
+W1W9C458C72XM1ULP1BVXPR
+LR0UF5R33VN4DZPJUGQA0CY
+A9WFKNJAW5K0D1SJNVY2DAW
+UZ8TNXA91J8GASZEG3GXZQM
+05DZ47KFJKPW6FRTJB1408J
+37XQMR4EJ7RLDDDD3PZWWXY
+NVGJTTW0GZCYKC07BDVQHVR
+7MK2P0CJRR4DNVLY0K564T4
+L4S2NBED58K86MK7C9Y42ZT
+LMB4TR1HPB3C6F6HDWKJ4A2
+NRTFR3Q4K4EDHH8WM2ADAKA
+QLPLCN13Z2HC8ELBJ2HWZWR
+KCWQE1KNVC3KW7XZGRVAU5Q
+69GSLNNEC0N3FU4CA09EEGE
+LQLPJR7VXCNGQZHZXM3Q09E
+ZABXFHVGNKMBSF1KZD1JYGD
+MH29T4T2BQW3BCU6BEZVNWB
+SG0FS4R1W2TSG6V7ZU00UVD
+5R99FUV2X6DU0QQFMACMV37
+AMPHQR8S0Q5D85XFZXS9WM8
+AU1325QFCDMD92BVJ1UNKUB
+R9X4QMJ11NA9THEXPSCFCES
+5MPD73H81E0ND5PAF9QTTA1
+R7UGAY5BBNP7PFBUK9F0UB2
+PLRAMYCRMTM8ENT7LDXZG4R
+U4MJ3WGBZ4P04G14C0VPF1T
+CFNQEFSUEKN1FLN9LMF5T7W
+JBNZREJAFVY82LMU09H59NH
+FH641QN4A5R139B1XDVR95H
+71WJ7TL1SGBR1X4HK01RB3A
+53SQJPFBWWM2QPHD7B5Y1D9
+HP56RB9QH0N0KFQDXDFJN4J
+YFNL1WXSLQLBGY8H6H7SQKZ
+N0K4B0CRE8XGFQ6R2J258XE
+6E4MMLDN7PKYKVBC5HY84UB
+LH4B02LJE3V4QWNBC92P36T
+46K1R5XQJ8DNFL2S9PSVE05
+KTNP7KXY8P3BH4GJCBCHAW6
+WFEX4UK60WTQDJSNQ2BSJRM
+LMVKNTH5SVTV4B6LNNZW3H8
+CWGQBAF6AZ82NU8U47MKE7Q
+WNYXQQX0WEEVQTF8JH690S1
+5WJA3TF43HKWSBUUNB9F1A5
+CL78PV3T9JTFDNF1Y9CA9YS
+NZPF8XE8562BCPQ3E4BLMCT
+HHZBNN0US3LG1MUR6A7U1L4
+40ZS9086PMMYHEZZCMD8E19
+LJZXQ3JX5LNSBLDA3B2HAZZ
+2KG86G8HC09KQ6AG3NP658C
+KEW6N6ANMUABGEP6YPEAHH2
+V2TGMTNW2PKSSCR9J7V0GU8
+X40UEV1PEN3FJY7WXVS3M57
+QXYMX3Q8J48KN0SSMKK6F7V
diff --git a/asset/data/promotion_codes-yb.csv b/asset/data/promotion_codes-yb.csv
new file mode 100644
index 0000000..02dd2e8
--- /dev/null
+++ b/asset/data/promotion_codes-yb.csv
@@ -0,0 +1,101 @@
+Promotion code
+-- KJM6GH5NTDDW12VDWK3D7AU
+-- 0CZG2AWS41G29VD2S1C3FBQ
+9H499GCQKXKEPMWTE6NYJ4L
+09KHJXX8Y4AE3A4DPZSCZB0
+HT7GJR81R2Q5XWGD81E0ZY1
+ZJNPW2ACQQQVJDPDNG1JYYR
+Q8BTA7DTADKF0S7YR8F6EKM
+ECZRD2BU74JL3T94ZD04C4Z
+NMLGBZP92MN9VH15FH4PJJK
+PSUPGVX1BFPDAM5BPCS3FQQ
+40PJBGMGJR0UHLEWBNWF32U
+W7N5D66YFKNNRXE6CJNXDT7
+A1Y1V52C3WLUXFFL4F42LVZ
+FM7EZK3HH6TN5YE702Z9AM5
+D5YSSBDF982P88YBGUNLFWF
+JCY6JHSXGP3XVT471GS32TB
+RMAH0VTA38WGR0YECTE6H09
+DTLSFDX7ZAA4HPEJJCKEZF5
+P8ZR8821XMNTMFN66DYG825
+THHUYPZ17EV00G2NM84EJWR
+E7ZHT83XN6KCXPQ3KRXCL0D
+R8BDSFN2F4GLNZU2V5QMSFF
+7VWXPULDVL9LGM7TZRKCBQP
+BD4UA5YKAV29CA8GN014RDQ
+4W8GX1EKCVEH17SYZ9MFX85
+SZ8DL9G6SCMCHMS7JA7HCDT
+D383P1Y0RCN5M49G454XJ4G
+7DC9FKU6PLYL7ARNL9FUMBX
+80RFXJF8ZHC6S4X592BTDR1
+VXJ9T0CGE2RLPSCRHGVDLK8
+W7LGSSRMEGGLDDT568WBMUT
+Q5RG5QTLG0N285KAD43QGNN
+QUYKWTNZ9MBQ1EL49WS2FB6
+GEQSQ8W0SHDK52RJTHBS45E
+DZNBW0L2M6MF8JCKVT15SM9
+KHL0A6MKBY4WF4KGYVSEXU9
+HWEVEH5QS74RD06XGSXUP4R
+CF1HCXMYWNT0GEK757DR7QJ
+38NSEXPC1XG573M6FVPU1DS
+RZ1QJ6TA68N0SPUT0FM6TYR
+SRHCQJ4ANJKLHH9WV127VTU
+RYD0XV94L770S9W1RW22AZY
+EM6TWEC71ATHZRU159H577W
+BZPJDHBUA0F37ULKUYKX3MD
+SL5PSDXWFKMWRSB1VTVK982
+H630F22CDLKGTE7128ZSR7V
+FXP9FC311KSFDX87KA7NDNS
+4ZQX57DTQ82T7B2A113RRA2
+GF0X3UK29CRPFUSMTKJY7VJ
+AHSZLDVUCDMXXC840DDERCX
+T9C5PVP0AMS48UW88L9Q1BQ
+G6SKQQK9EAVJF1TM4J2SK4H
+3Y00PYF2QKRQN4ZNZVWERGH
+QCSEMNG7G0704UHE93KENLH
+MUB9P4Y1F5VLPTSGRT6E1ED
+KE9EC1JH63LBX79BHX1WQFH
+PD16F8BZZRC94TFJRQ3L3PG
+ANQDQ2KJTVQQTYXPAUELB1S
+RAXT8YFK3LAALY0HLWX9KAQ
+4CR33PYZZ7RGKWDTCFD9XVK
+NWV7CJX6AG78NR7769D190Y
+EC490NHL19NZV0GF76TLQXQ
+9E42VMUFB13NU6N8QGL2TYT
+ED99UQ1VFVJSHPGXC8ERAEA
+XZU9XZK7RKL8GRTFS5LUDXD
+677FF9GZVHEYWC13N5HGSGD
+NJVAWAYGEZAL6JCVB0D43CD
+CJ4JGJZQW5JVLN5BDGKGLT6
+VHB93V8U4ASWS78ZUYZCN19
+3JGM2U3EPZ8MW4TBZW7H53G
+2FYZ7G4JRWEMZMDPQQSJUTV
+5E3C44SU9R3YLBJDGR3GQM8
+S1X2HWGFDDL61A48EY777DU
+5MHRAX98UTQQ372ANJ3WJB0
+X5C7UB37VQ3R4PFT38FDAA5
+PFG41S3LRSXPQ9ZFMKFTGDQ
+TRXZU2HEVR1TPXXQEGNU611
+0C4ZVMKCHVJVKASB78X6CYL
+ZVHV1LA2JZXD90CEP375W8R
+R9TGDA50REYPP4FKCF9GK6H
+DGDYA7XVR0SLX6CWWMSVVDB
+65XLN0P4LBQ6J81VJ1JD0CH
+RYV7SZLD8B6N92QA8V4PT5N
+RDK07CND4QR8QRCWNZMEMJA
+NKNJ09BPS7M4E4CJ4H3WUX7
+554X7A7Z0F4MPS8NX8H5GLG
+5XQZ5L9Y8BTB2C4DRQKDW53
+9T9KK0DUQQL7Z3H19HCFM4C
+ZBF62Y2PF1M5T2B69PEKECR
+DVG93BR8BVT0DW3V10USX3H
+4DUQ0CAMF0S08YBVWTG4Z6Y
+0EC19Q18NTVE1S8NN7EWFSQ
+L9617J33F5GU2LRK4HQ0AY3
+D8MHJSK6E86QCLFYFBMJ63K
+AJ8ZQK2SYGK6YK4ZWAJPV5H
+WL7NJ28NRYQ4J36F13HX65W
+VP5MTQZB78ZGJ5SKUQYJ67Y
+6DL9WDQ3U1A8A9Z30B05KZL
+VXF8A7QMEQCLQAY9P7B5EZA
+KCNPFPUHRFXPZWCACFQ8YW6
diff --git a/asset/data/promotion_codes_ya.csv b/asset/data/promotion_codes_ya.csv
new file mode 100644
index 0000000..710c3fd
--- /dev/null
+++ b/asset/data/promotion_codes_ya.csv
@@ -0,0 +1,101 @@
+Promotion code
+DULPYTPABG7Y51GYL6A8Z72
+8A8PH0M87ESVL2YTMXXFVKJ
+8X2976ZLFR61SP85NGVK0AA
+8CDC60CU9HLBNJEQ9F2QPC7
+VSHTMHLJTBAV731JX52MNQ4
+C302XQN62ZU8F27BQ1RBGT9
+6FDAGQVCGZQSCA56XXA08DM
+KK0KBEUGWW3521NA31GV3T4
+PAT2TF5A44RAMD883ECHYCG
+CGRHCDVKA9Z8D7RXV97BRPY
+E1FQ4V968KYEU6JDQ8TQEAT
+NEH6BUEAT9UCHAX1QJD0CJJ
+QHLBD3GN473MHT30QAAD0U8
+2XK0LYH77U6EUQ3NM8M2U1C
+CG6C3H6BK4RZAFS9WF46YU0
+BY6Y07P0AYT5TH2VQ8X267N
+9C5VW5X5DHHKJ8Q40ZM81EG
+P5Y8K895CP4AVBYVDCG5D60
+T5S0M4LN7SAX27B4RKDGKQ3
+6G34MCK50FNLZ8ZUTWLF5AJ
+JZD22RSD7WRKXLP8EDBWH4U
+7V4WT93MFY03AB76NJH8SXJ
+CY4VKZ74MYUEM3KTGL0LHLT
+XP4U1930D44QAUUEM16PLCT
+U5M7FPV2MHUA6VEJN2XWVKY
+D6EC4QWWXDYUULAW8QT8AHF
+LJ4NJAY7KCM25WT0A25BPN6
+K6SQLF2C9APJBZF04BRDNH2
+1C8L76C512HBULG3NQUA84K
+H1VR3131G5G2ARSLGVMM4GF
+7AHJ0EB97PA4XY1GLTVTTHN
+AUWZGVK4ULY52210Y8ULD30
+D0JY0HW6MLHAPCKBQTMVFQ3
+TSE722BT94HNSY3GRV28UN1
+H8Q1EAYYS5JDX2KU4J3P344
+U3K5S83M1ULR0ETL8XFJ8VJ
+XEG86F51SXXY5P2HZG49H58
+8HFS2KE099XZMHYEDEQN84Z
+3GUH12E4V5RYV9J8ZMBETZ8
+DHKTFWBNJ7R1CXV2E41K3CK
+49KK8J5Y1K2LPERB6G09P9M
+VXYYBTMLPXAY4EGUENVLAZR
+5AHZGKA02K4717MD2A94EPJ
+7DGNRVD16Q2GP5N9XL3Y0EG
+7LG8U55NRA6TKRT3VPY1RME
+S8RY3KSDJPMW0Y26VEC4RVG
+WS043ADBZ4HHZ57U9SZRXUP
+K7YET0Q249XNFH7NX8GX575
+UEJCE8CMDSUKJNBTVN9NUBA
+UDQA1SY0L9FRHZBKR8GHWEN
+XM4K7S04G018P3NQHS8TV2Y
+9LJT7CSYTMJALZ6L7TUGP3R
+SRN1UNBW6BSAYUFFJ7MQ690
+7XHX75BCBMK5R1WKHDFJX2P
+RULRS8H8LKFZY8VVMRRD8DM
+N1Y3DSPBCC6CZTKP5NUMZ1D
+H4RAWV8Z8ZCS80QQMQ39AN9
+URF18Q5GTQ0A5WJG0MUCJ5G
+QPBHN1B2JNQB2THW2NWDMS5
+C6N0Z1D7SA7H9HLABSA2W5Q
+31CQANBKBPPUZ8FLHCYJ2WB
+GHGJV049LN066UYM239K4SV
+4NDWC74YQJKFJZHSHP6N4YG
+904EM94E8VEFLWWLT10W7C8
+HRLLGB3QHCW8PG75XWL9CV8
+SHEYF4F7B5F6LGLWGXYMBKV
+WABU07E42Z3FK2V06XLDQDC
+40KX6L2T1R839HWQHF8UWWU
+Q8XAXGQEZ2V847WMYKCV91A
+LK1MV8G85EV51XA43HVW5AA
+U5HB1KMVS1AUD21YE4ZXDS5
+5Y5SKU4MP2QGTUPKG3LEL8K
+FC8VS4S0SZ1VJUH4SNDKAQ4
+WUS35TBS78VWS4SSJQPG502
+2FZQLEF358H0LA2HGQAB57W
+KQVC3CHDNPDR1ADVDNAZDCH
+0AFT33YM8X7RTXT07KVVN21
+GP2M6QAM3G18MBC2ZYD73RL
+GFKSN039H70CYT70JM81SKQ
+2ECADR3TXBJ2CUWF6WKDF7L
+U2V944D5TB4TZXG6JCD003P
+PHG6QP4YT5TJZ75F58QADQ8
+GUVJVYANS8HPWQHMTNGHXGA
+USAL3QBLYWPCC0JXDQGK263
+V5M18UT9L2Q3EZ095TUAPQX
+W3UCUV9WW5HEG4UBX1M0U58
+YBSM2K271EJHJ23CSD6HS04
+HT8RFH4TMX9LJVA3FJDNRCG
+9HDPRJHG15E5275PDKXAFB7
+R95YEGEU7U8DR6DQUYMBKTH
+3LCXQ54B24GH4K5WU6G3DU9
+NHHTHNT2C42CARLX2RLFMUS
+WB5EW890X513DLFEW0QZX5M
+WDB6FSAFZ8JXLPKUF2JLWRB
+346AWN92NCYTK12NNV1SYMK
+V0TQG3F0XYBDPU946C13LMQ
+SA4HAPXW1TPVAFCG5P9H6Q3
+SMJJE6RY9W6Q6HWHEL7YSHU
+AHA16TP02C6NS83A8DSA9B2
+NSJVXY2H026YQB3ZJMZLY2E
diff --git a/i18n/en.json b/i18n/en.json
index 48fd81d..c324114 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -9,7 +9,7 @@
"Logout": "Logout",
"Tests": "Tests",
"Change Language": "Change Language",
- "Password too short": "Password too short (at least 6 characters)",
+ "Password too short": "Password too short (at least 9 characters)",
"Please type an email address": "Please type an email address",
"Exception: Please accept our data policy":"Please accept our data policy",
"Please accept our data protection policy. For more information please click on 'Privacy'":"Please accept our data protection policy. For more information please click on 'Privacy'",
@@ -27,6 +27,8 @@
"Please log in": "Please log in",
"Exception: Customer does not exist or the password is wrong": "The email does not exist or the password is wrong",
"Exception: Customer exists": "The email address has been registered already",
+ "Exception: Please type an email address": "Please type an email address",
+ "Exception: Password too short": "Password too short (at least 9 characters)",
"There is an error: during registration:": "There is an error: during registration:",
"Please select an exercise": "Please select an exercise",
"Cardio": "Cardio",
@@ -162,7 +164,7 @@
"Edit My Custom Plan": "Edit My Custom Plan",
"Suggested Training Plan": "Suggested Training Plan",
"My Special Plan": "My Special Plan",
- "Star's Exercise Plan":"Star's Exercise Plan",
+ "Star's Exercise Plan":"Celeb Exercise Plan",
"My Trainee's Plan": "My Trainee's Plan",
"Execute My Trainee's Training Plan": "Execute My Trainee's Training Plan",
diff --git a/i18n/hu.json b/i18n/hu.json
index 79963ee..8c82843 100644
--- a/i18n/hu.json
+++ b/i18n/hu.json
@@ -9,7 +9,7 @@
"Login": "Bejelentkezés",
"Logout": "Kijelentkezés",
"Change Language": "Nyelv",
- "Password too short": "A jelszó min. 6 karakterből álljon",
+ "Password too short": "A jelszó min. 9 karakterből álljon",
"Please type an email address": "Kérlek írj be egy email címet",
"SignUp": "Regisztráció",
"Exception: Please accept our data policy":"Kérlek fogadd el az adatvédelmi szabályzatunkat",
@@ -29,6 +29,8 @@
"Customer does not exist or the password is wrong": "A felhasználó nem létezik vagy a jelszó rossz.",
"The email does not exist or the password is wrong": "A felhasználó nem létezik vagy a jelszó rossz.",
"Exception: Facebook login was not successful": "Facebook bejelentkezés sikertelen",
+ "Exception: Please type an email address": "Kérlek írj be egy email címet",
+ "Exception: Password too short": "A jelszó min. 9 karakterből álljon",
"Exception: You have a previous Facebook login operation in progress":"Az előző bejelentkezés még folyamatban van.",
"Exception: Facebook login cancelled":"Facebook bejelentkezés megszakítva ",
"Exception: Facebook login failed":"Facebook bejelentkezés sikertelen",
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index cdc9f4f..b27c0d8 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -15,6 +15,7 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
BB69292B2521AF45001FBA4C /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BB69292A2521AF45001FBA4C /* Launch Screen.storyboard */; };
BB81345024BB4BE10078D9A4 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB81344F24BB4BE10078D9A4 /* GoogleService-Info.plist */; };
+ BB8D3BFA25A8CBFE00BF29FE /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB8D3BF925A8CBFE00BF29FE /* AuthenticationServices.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@@ -48,6 +49,7 @@
BB43773E2540715900D74BFA /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
BB69292A2521AF45001FBA4C /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; };
BB81344F24BB4BE10078D9A4 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
+ BB8D3BF925A8CBFE00BF29FE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; };
D5EDDC52125075FB9E21AD35 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
F39E6E227EB942E5663A6086 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -57,6 +59,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ BB8D3BFA25A8CBFE00BF29FE /* AuthenticationServices.framework in Frameworks */,
42B6B159AF35AFB6DE777DFB /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -77,6 +80,7 @@
3ADC50290ED054951FAC1F56 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ BB8D3BF925A8CBFE00BF29FE /* AuthenticationServices.framework */,
09BD889296C5C90D989820C8 /* Pods_Runner.framework */,
);
name = Frameworks;
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 903def2..f166400 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -4,5 +4,7 @@
aps-environment
development
+ com.apple.developer.authentication-services.autofill-credential-provider
+
diff --git a/lib/bloc/sales/sales_bloc.dart b/lib/bloc/sales/sales_bloc.dart
index 8e26f1d..b37c91a 100644
--- a/lib/bloc/sales/sales_bloc.dart
+++ b/lib/bloc/sales/sales_bloc.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'dart:math';
import 'package:aitrainer_app/model/cache.dart';
@@ -6,6 +7,7 @@ import 'package:aitrainer_app/model/product.dart';
import 'package:aitrainer_app/model/product_test.dart';
import 'package:aitrainer_app/service/logging.dart';
import 'package:aitrainer_app/service/product_test_service.dart';
+import 'package:aitrainer_app/util/platform_purchase.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flurry/flurry.dart';
@@ -26,20 +28,46 @@ class SalesBloc extends Bloc with Logging {
try {
if (event is SalesLoad) {
yield SalesLoading();
- Flurry.logEvent("SalesPageOpen");
+ //Flurry.logEvent("SalesPageOpen");
+ await PlatformPurchaseApi().initPurchasePlatformState();
this.getProductSet();
yield SalesReady();
} else if (event is SalesPurchase) {
final int productId = event.productId;
trace("Requesting purchase for" + productId.toString());
- Flurry.logEvent("PurchaseRequest");
- //PlatformPurchaseApi().requestPurchase(null);
+ //Flurry.logEvent("PurchaseRequest");
+ PlatformPurchaseApi().purchase(getSelectedProduct(productId));
}
} on Exception catch (ex) {
yield SalesError(message: ex.toString());
}
}
+ Product getSelectedProduct(int productId) {
+ Product prod;
+ for (var product in this.product2Display) {
+ if (product.productId == productId) {
+ prod = product;
+ }
+ }
+ return prod;
+ }
+
+ String getLocalizedPrice(String productId) {
+ String price = "";
+ for (var product in PlatformPurchaseApi().getIAPItems()) {
+ if (Platform.isAndroid) {
+ print("PlatformProduct " + product.toString());
+ if (productId == product.productId) {
+ price = product.localizedPrice;
+ break;
+ }
+ }
+ }
+
+ return price;
+ }
+
void getProductSet() {
int productId = 0;
this.tests = Cache().productTests;
@@ -51,7 +79,7 @@ class SalesBloc extends Bloc with Logging {
trace("Previous ProductTest: " + tests[0].toJson().toString());
productId = tests[0].productId;
for (var elem in Cache().products) {
- Product product = elem as Product;
+ final Product product = elem as Product;
if (product.productId == productId) {
productSet = product.productSet;
break;
@@ -63,8 +91,11 @@ class SalesBloc extends Bloc with Logging {
trace("ProductSet: " + productSet.toString());
for (var elem in Cache().products) {
Product product = elem as Product;
+
if (product.productSet == productSet) {
productId = product.productId;
+ final String platformProductId = Platform.isAndroid ? product.productIdAndroid : product.productIdIos;
+ product.localizedPrice = getLocalizedPrice(platformProductId);
product2Display.add(product);
}
}
@@ -76,7 +107,7 @@ class SalesBloc extends Bloc with Logging {
productTest.productId = productId;
productTest.customerId = Cache().userLoggedIn.customerId;
productTest.dateView = DateTime.now();
- ProductTestApi().saveProductTest(productTest);
- Cache().productTests.add(productTest);
+ //ProductTestApi().saveProductTest(productTest);
+ //Cache().productTests.add(productTest);
}
}
diff --git a/lib/bloc/session/session_bloc.dart b/lib/bloc/session/session_bloc.dart
index 281a1be..5f866bf 100644
--- a/lib/bloc/session/session_bloc.dart
+++ b/lib/bloc/session/session_bloc.dart
@@ -28,7 +28,6 @@ class SessionBloc extends Bloc with Logging {
// ignore: close_sinks
SettingsBloc settingsBloc = event.settingsBloc;
await session.fetchSessionAndNavigate();
- await PlatformPurchaseApi().initPurchasePlatformState();
String lang = AppLanguage().appLocal.languageCode;
log("Change lang to $lang");
settingsBloc.add(SettingsChangeLanguage(language: lang));
diff --git a/lib/model/cache.dart b/lib/model/cache.dart
index f81821f..7f68fac 100644
--- a/lib/model/cache.dart
+++ b/lib/model/cache.dart
@@ -105,7 +105,7 @@ class Cache with Logging {
LinkedHashMap _badges = LinkedHashMap();
List deviceLanguages;
- String startPage;
+ String startPage = "home";
String testEnvironment;
bool liveServer = true;
bool hasHardware = false;
diff --git a/lib/model/product.dart b/lib/model/product.dart
index ab29866..5acba9a 100644
--- a/lib/model/product.dart
+++ b/lib/model/product.dart
@@ -12,6 +12,7 @@ class Product {
String productIdAndroid;
double priceIos;
double priceAndroid;
+ String localizedPrice;
Product.fromJson(Map json) {
this.productId = json['productId'];
diff --git a/lib/repository/workout_tree_repository.dart b/lib/repository/workout_tree_repository.dart
index 314ac6c..28c2e0a 100644
--- a/lib/repository/workout_tree_repository.dart
+++ b/lib/repository/workout_tree_repository.dart
@@ -10,6 +10,7 @@ import 'package:aitrainer_app/service/exercise_tree_service.dart';
import 'package:aitrainer_app/service/exercisetype_service.dart';
import 'package:aitrainer_app/service/logging.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
class Antagonist {
static String chest = "Chest";
@@ -47,6 +48,19 @@ class WorkoutTreeRepository with Logging {
Antagonist.calf: Antagonist.calfNr
};
+ Future _buildImage(String imageUrl) async {
+ String assetImage = 'asset/menu/' + imageUrl.substring(7);
+ //print("Loading image " + assetImage);
+ return rootBundle.load(assetImage).then((value) {
+ return assetImage;
+ }).catchError((_) {
+ String imagePath = assetImage.substring(10);
+ String url = Cache.mediaUrl + 'images' + imagePath;
+ //print("Exception: " + assetImage + " will be loaded from the network " + url);
+ return url;
+ });
+ }
+
Future createTree() async {
isEnglish = AppLanguage().appLocal == Locale('en');
log("** Start creating tree on lang: " + AppLanguage().appLocal.languageCode);
@@ -65,7 +79,7 @@ class WorkoutTreeRepository with Logging {
//log(" -- TreeItem " + treeItem.toJson().toString() + " active " + treeItem.active.toString());
if (treeItem.active == true) {
String treeName = isEnglish ? treeItem.name : treeItem.nameTranslation;
- String assetImage = 'asset/menu/' + treeItem.imageUrl.substring(7);
+ //String assetImage = await _buildImage(treeItem.imageUrl);
bool is1RM = treeItem.name == 'One Rep Max' ? true : false;
if (is1RM == false && treeItem.parentId != 0) {
@@ -86,7 +100,7 @@ class WorkoutTreeRepository with Logging {
treeItem.treeId,
treeItem.parentId,
treeName,
- assetImage,
+ treeItem.imageUrl,
Colors.white,
30,
false,
@@ -104,11 +118,11 @@ class WorkoutTreeRepository with Logging {
}
});
- exerciseTypes.forEach((exerciseType) {
+ exerciseTypes.forEach((exerciseType) async {
if (!(exerciseType.imageUrl.isEmpty || exerciseType.name.isEmpty || exerciseType.nameTranslation.isEmpty) &&
exerciseType.active == true) {
String exerciseTypeName = isEnglish ? exerciseType.name : exerciseType.nameTranslation;
- String assetImage = 'asset/menu/' + exerciseType.imageUrl.substring(7);
+ //String assetImage = await _buildImage(exerciseType.imageUrl); //'asset/menu/' + exerciseType.imageUrl.substring(7);
if (exerciseType.parents.isNotEmpty) {
exerciseType.parents.forEach((parentId) {
bool is1RM = this.isParent1RM(parentId);
@@ -117,8 +131,21 @@ class WorkoutTreeRepository with Logging {
if (isEndurance) exerciseType.setAbility(ExerciseAbility.endurance);
bool isRunning = this.isParentRunning(parentId);
if (isRunning) exerciseType.setAbility(ExerciseAbility.running);
- WorkoutMenuTree menuItem = WorkoutMenuTree(exerciseType.exerciseTypeId, parentId, exerciseTypeName, assetImage, Colors.white,
- 24, true, exerciseType.exerciseTypeId, exerciseType, exerciseType.base, is1RM, isEndurance, isRunning, exerciseType.name);
+ WorkoutMenuTree menuItem = WorkoutMenuTree(
+ exerciseType.exerciseTypeId,
+ parentId,
+ exerciseTypeName,
+ exerciseType.imageUrl,
+ Colors.white,
+ 24,
+ true,
+ exerciseType.exerciseTypeId,
+ exerciseType,
+ exerciseType.base,
+ is1RM,
+ isEndurance,
+ isRunning,
+ exerciseType.name);
this.tree[exerciseType.name] = menuItem;
//log("WorkoutMenuTree item " + menuItem.toJson().toString());
/* log("ExerciseType in Menu item " +
diff --git a/lib/service/exercise_tree_service.dart b/lib/service/exercise_tree_service.dart
index 1b83ead..7bbf7eb 100644
--- a/lib/service/exercise_tree_service.dart
+++ b/lib/service/exercise_tree_service.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/model/exercise_tree.dart';
import 'package:aitrainer_app/model/exercise_tree_parents.dart';
+import 'package:flutter/services.dart';
import 'api.dart';
class ExerciseTreeApi {
@@ -15,10 +16,28 @@ class ExerciseTreeApi {
exerciseTree = await getExerciseTreeParents(exerciseTree);
+ if (exerciseTree != null) {
+ exerciseTree.forEach((element) async {
+ element.imageUrl = await _buildImage(element.imageUrl);
+ });
+ }
Cache().setExerciseTree(exerciseTree);
return exerciseTree;
}
+ Future _buildImage(String imageUrl) async {
+ String assetImage = 'asset/menu/' + imageUrl.substring(7);
+ //print("Loading image " + assetImage);
+ return rootBundle.load(assetImage).then((value) {
+ return assetImage;
+ }).catchError((_) {
+ String imagePath = assetImage.substring(10);
+ String url = Cache.mediaUrl + 'images' + imagePath;
+ //print("Exception: " + assetImage + " will be loaded from the network " + url);
+ return url;
+ });
+ }
+
Future> getExerciseTreeParents(List exerciseTree) async {
List copyList = this._copyList(exerciseTree);
diff --git a/lib/service/exercisetype_service.dart b/lib/service/exercisetype_service.dart
index 6afe41b..3d72e18 100644
--- a/lib/service/exercisetype_service.dart
+++ b/lib/service/exercisetype_service.dart
@@ -3,6 +3,7 @@ import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/model/exercise_type.dart';
import 'package:aitrainer_app/service/api.dart';
import 'package:aitrainer_app/service/logging.dart';
+import 'package:flutter/services.dart';
class ExerciseTypeApi with Logging {
final APIClient _client = new APIClient();
@@ -11,10 +12,31 @@ class ExerciseTypeApi with Logging {
final body = await _client.get("exercise_type", "");
final Iterable json = jsonDecode(body);
final List exerciseTypes = json.map((exerciseType) => ExerciseType.fromJson(exerciseType)).toList();
+ if (exerciseTypes != null) {
+ exerciseTypes.forEach((element) async {
+ element.imageUrl = await _buildImage(element.imageUrl);
+ });
+ }
Cache().setExerciseTypes(exerciseTypes);
return exerciseTypes;
}
+ Future _buildImage(String imageUrl) async {
+ if (imageUrl.length > 8) {
+ String assetImage = 'asset/menu/' + imageUrl.substring(7);
+ return rootBundle.load(assetImage).then((value) {
+ return assetImage;
+ }).catchError((_) {
+ String imagePath = assetImage.substring(10);
+ String url = Cache.mediaUrl + 'images' + imagePath;
+ //print("Exception: " + assetImage + " will be loaded from the network " + url);
+ return url;
+ });
+ } else {
+ return imageUrl;
+ }
+ }
+
Future saveExerciseType(ExerciseType exerciseType) async {
String body = JsonEncoder().convert(exerciseType.toJson());
log(" ===== saving exerciseType id: " + exerciseType.exerciseTypeId.toString() + ":" + body);
diff --git a/lib/service/firebase_api.dart b/lib/service/firebase_api.dart
index 07669db..a4233dd 100644
--- a/lib/service/firebase_api.dart
+++ b/lib/service/firebase_api.dart
@@ -35,6 +35,12 @@ class FirebaseApi with Logging {
}
Future signInEmail(String email, String password) async {
+ if (email == null) {
+ throw Exception("Please type an email address");
+ }
+ if (password == null) {
+ throw Exception("Password too short");
+ }
String rc = SIGN_IN_OK;
try {
userCredential = await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password);
diff --git a/lib/util/platform_purchase.dart b/lib/util/platform_purchase.dart
index 02cbf3d..57d8246 100644
--- a/lib/util/platform_purchase.dart
+++ b/lib/util/platform_purchase.dart
@@ -1,5 +1,8 @@
import 'dart:async';
+import 'dart:io';
+import 'package:aitrainer_app/model/cache.dart';
+import 'package:aitrainer_app/model/product.dart';
import 'package:aitrainer_app/service/logging.dart';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
@@ -11,10 +14,14 @@ class PlatformPurchaseApi with Logging {
StreamSubscription _conectionSubscription;
String _platformVersion = 'Unknown';
List _items = [];
+ List getIAPItems() => _items;
List _purchases = [];
- final List _productLists = List();
- /* Platform.isAndroid
+ final List _productList = List();
+
+ List getProductList() => _productList;
+
+ /* Platform.isAndroid
? [
'android.test.purchased',
'point_1000',
@@ -22,7 +29,7 @@ class PlatformPurchaseApi with Logging {
'android.test.canceled',
]
: ['com.cooni.point1000', 'com.cooni.point5000'];
- */
+ */
factory PlatformPurchaseApi() {
return _singleton;
@@ -30,27 +37,32 @@ class PlatformPurchaseApi with Logging {
PlatformPurchaseApi._internal();
- void close() {
+ Future close() async {
if (_conectionSubscription != null) {
_conectionSubscription.cancel();
_conectionSubscription = null;
}
+ await FlutterInappPurchase.instance.endConnection;
}
// Platform messages are asynchronous, so we initialize in an async method.
Future initPurchasePlatformState() async {
+ if (_productList.length > 0) {
+ return;
+ }
+ log(" --- Init PurchasePlatform");
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
try {
platformVersion = await FlutterInappPurchase.instance.platformVersion;
} on Exception {
platformVersion = 'Failed to get platform version for InAppPurchase.';
- log(platformVersion);
+ log("PlatformVersion" + platformVersion);
}
// prepare
var result = await FlutterInappPurchase.instance.initConnection;
- log('result: $result');
+ log(' FlutterInappPurchase init result: $result');
_platformVersion = platformVersion;
@@ -73,15 +85,57 @@ class PlatformPurchaseApi with Logging {
_purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) {
log('purchase-error: $purchaseError');
});
+
+ getSubscriptions();
+ }
+
+ void getSubscriptions() {
+ Cache().products.forEach((element) {
+ Product product = element as Product;
+ if (Platform.isAndroid) {
+ if (product.productIdAndroid != null && product.productIdAndroid.isNotEmpty) {
+ _productList.add(product.productIdAndroid);
+ }
+ } else {
+ if (product.productIdIos != null && product.productIdIos.isNotEmpty) {
+ _productList.add(product.productIdIos);
+ }
+ }
+ });
+
+ _productList.forEach((element) {
+ print(element);
+ });
+ _getSubscription();
+ }
+
+ void purchase(Product product) {
+ if (product == null) {
+ throw Exception("No product to purchase");
+ }
+ String productId = Platform.isAndroid ? product.productIdAndroid : product.productIdIos;
+ IAPItem selected;
+ for (var item in _items) {
+ if (item.productId == productId) {
+ selected = item;
+ log("Item to purchase: " + item.toString());
+ }
+ }
+
+ if (selected != null) {
+ requestPurchase(selected);
+ } else {
+ throw Exception("product " + productId + " not defined in the PlatformStore");
+ }
}
void requestPurchase(IAPItem item) {
//FlutterInappPurchase.instance.requestPurchase(item.productId);
- FlutterInappPurchase.instance.requestPurchase("WT_monthly");
+ FlutterInappPurchase.instance.requestPurchase(item.productId);
}
- Future _getProduct() async {
- List items = await FlutterInappPurchase.instance.getProducts(_productLists);
+ Future _getSubscription() async {
+ List items = await FlutterInappPurchase.instance.getSubscriptions(_productList);
for (var item in items) {
log('${item.toString()}');
this._items.add(item);
diff --git a/lib/view/sales_page.dart b/lib/view/sales_page.dart
index ab73a98..c0cf855 100644
--- a/lib/view/sales_page.dart
+++ b/lib/view/sales_page.dart
@@ -1,4 +1,5 @@
import 'package:aitrainer_app/bloc/sales/sales_bloc.dart';
+import 'package:aitrainer_app/service/logging.dart';
import 'package:aitrainer_app/util/trans.dart';
import 'package:aitrainer_app/widgets/app_bar_min.dart';
import 'package:aitrainer_app/widgets/sales_button.dart';
@@ -8,121 +9,122 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:modal_progress_hud/modal_progress_hud.dart';
// ignore: must_be_immutable
-class SalesPage extends StatelessWidget with Trans {
+class SalesPage extends StatelessWidget with Trans, Logging {
@override
Widget build(BuildContext context) {
setContext(context);
- return BlocProvider(
- create: (context) => SalesBloc()..add(SalesLoad()),
- child: BlocConsumer(listener: (context, state) {
- if (state is SalesError) {
- Scaffold.of(context).showSnackBar(
- SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white))));
- }
- }, builder: (context, state) {
- final salesBloc = BlocProvider.of(context);
- return ModalProgressHUD(
- child: salesWidget(salesBloc),
- inAsyncCall: state is SalesLoading,
- opacity: 0.5,
- color: Colors.black54,
- progressIndicator: CircularProgressIndicator(),
- );
- }));
+ return Scaffold(
+ appBar: AppBarMin(
+ back: true,
+ ),
+ body: BlocProvider(
+ create: (context) => SalesBloc()..add(SalesLoad()),
+ child: BlocConsumer(listener: (context, state) {
+ if (state is SalesError) {
+ log("Error: " + state.message);
+ Scaffold.of(context).showSnackBar(
+ SnackBar(backgroundColor: Colors.orange, content: Text(state.message, style: TextStyle(color: Colors.white))));
+ }
+ }, builder: (context, state) {
+ final salesBloc = BlocProvider.of(context);
+ return ModalProgressHUD(
+ child: salesWidget(salesBloc),
+ inAsyncCall: state is SalesLoading,
+ opacity: 0.5,
+ color: Colors.black54,
+ progressIndicator: CircularProgressIndicator(),
+ );
+ })));
}
Widget salesWidget(SalesBloc bloc) {
final double mediaWidth = MediaQuery.of(context).size.width;
final double imageWidth = (mediaWidth - 5) / 2;
- return Scaffold(
- appBar: AppBarMin(
- back: true,
+ return Container(
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ image: AssetImage('asset/image/WT_black_background.png'),
+ fit: BoxFit.cover,
+ alignment: Alignment.center,
+ ),
),
- body: Container(
- decoration: BoxDecoration(
- image: DecorationImage(
- image: AssetImage('asset/image/WT_black_background.png'),
- fit: BoxFit.cover,
- alignment: Alignment.center,
- ),
+ child: CustomScrollView(scrollDirection: Axis.vertical, slivers: [
+ SliverList(
+ delegate: SliverChildListDelegate([
+ Divider(),
+ Container(
+ padding: EdgeInsets.only(left: 65, right: 65),
+ child: Text("Unleash Your Development Now!",
+ textAlign: TextAlign.center,
+ maxLines: 4,
+ softWrap: true,
+ style: GoogleFonts.archivoBlack(
+ fontSize: 30,
+ color: Colors.white,
+ shadows: [
+ Shadow(
+ offset: Offset(5.0, 5.0),
+ blurRadius: 12.0,
+ color: Colors.black54,
+ ),
+ Shadow(
+ offset: Offset(-3.0, 3.0),
+ blurRadius: 12.0,
+ color: Colors.black54,
+ ),
+ ],
+ ))),
+ Divider(),
+ Container(
+ padding: EdgeInsets.only(left: 45, right: 45),
+ child: Text("Learn about your development, enjoy AI-driven predictions of all of your skills and bodyparts.",
+ textAlign: TextAlign.left,
+ maxLines: 4,
+ softWrap: true,
+ style: GoogleFonts.inter(
+ fontSize: 16,
+ color: Colors.white,
+ ))),
+ SizedBox(
+ height: 50,
),
- child: CustomScrollView(scrollDirection: Axis.vertical, slivers: [
- SliverList(
- delegate: SliverChildListDelegate([
- Divider(),
- Container(
- padding: EdgeInsets.only(left: 65, right: 65),
- child: Text("Unleash Your Development Now!",
- textAlign: TextAlign.center,
- maxLines: 4,
- softWrap: true,
- style: GoogleFonts.archivoBlack(
- fontSize: 30,
- color: Colors.white,
- shadows: [
- Shadow(
- offset: Offset(5.0, 5.0),
- blurRadius: 12.0,
- color: Colors.black54,
- ),
- Shadow(
- offset: Offset(-3.0, 3.0),
- blurRadius: 12.0,
- color: Colors.black54,
- ),
- ],
- ))),
- Divider(),
- Container(
- padding: EdgeInsets.only(left: 45, right: 45),
- child: Text("Learn about your development, enjoy AI-driven predictions of all of your skills and bodyparts.",
- textAlign: TextAlign.left,
- maxLines: 4,
- softWrap: true,
- style: GoogleFonts.inter(
- fontSize: 16,
- color: Colors.white,
- ))),
- SizedBox(
- height: 50,
- ),
- ])),
- SliverGrid(
- delegate: SliverChildListDelegate(getButtons(bloc)),
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- mainAxisSpacing: 10.0,
- crossAxisSpacing: 10.0,
- childAspectRatio: 0.55,
- ),
- ),
- SliverList(
- delegate: SliverChildListDelegate([
- SizedBox(
- height: 30,
- ),
- Container(
- padding: EdgeInsets.only(left: 55, right: 55),
- child: Text(
- "Subscription Conditions",
- style: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
- )),
- Divider(),
- Container(
- padding: EdgeInsets.only(left: 55, right: 55),
- child: Text(
- "Payment will be charged to your account. Subscription automatically renews unless auto-renew is turned off at least 24 hourse before the end of the current period",
- style: GoogleFonts.inter(fontSize: 12, color: Colors.white),
- )),
- Divider(),
- Container(
- padding: EdgeInsets.only(left: 55, right: 55),
- child: Text(
- "Account will be charged for renewal within 24 hours prior to the end of the current period",
- style: GoogleFonts.inter(fontSize: 12, color: Colors.white),
- )),
- ])),
- ])));
+ ])),
+ SliverGrid(
+ delegate: SliverChildListDelegate(getButtons(bloc)),
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3,
+ mainAxisSpacing: 10.0,
+ crossAxisSpacing: 10.0,
+ childAspectRatio: 0.55,
+ ),
+ ),
+ SliverList(
+ delegate: SliverChildListDelegate([
+ SizedBox(
+ height: 30,
+ ),
+ Container(
+ padding: EdgeInsets.only(left: 55, right: 55),
+ child: Text(
+ "Subscription Conditions",
+ style: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
+ )),
+ Divider(),
+ Container(
+ padding: EdgeInsets.only(left: 55, right: 55),
+ child: Text(
+ "Payment will be charged to your account. Subscription automatically renews unless auto-renew is turned off at least 24 hourse before the end of the current period",
+ style: GoogleFonts.inter(fontSize: 12, color: Colors.white),
+ )),
+ Divider(),
+ Container(
+ padding: EdgeInsets.only(left: 55, right: 55),
+ child: Text(
+ "Account will be charged for renewal within 24 hours prior to the end of the current period",
+ style: GoogleFonts.inter(fontSize: 12, color: Colors.white),
+ )),
+ ])),
+ ]));
;
}
@@ -141,7 +143,7 @@ class SalesPage extends StatelessWidget with Trans {
}
Widget button = SalesButton(
title: title,
- price: element.description + interval,
+ price: element.localizedPrice,
desc1: "Development programs",
desc2: "Suggestions based on your actual status",
desc3: "Special customized training plans",
diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart
index 23558a4..3f6a4fc 100644
--- a/lib/widgets/app_bar.dart
+++ b/lib/widgets/app_bar.dart
@@ -142,6 +142,9 @@ class _AppBarNav extends State with SingleTickerProviderStateMixin, C
ExerciseRepository exerciseRepository = ExerciseRepository();
exerciseRepository.getBaseExerciseFinishedPercent();
percent = Cache().getPercentExercises();
+ if (percent == null || percent == -1) {
+ percent = 0;
+ }
}
int sizeExerciseList = Cache().getExercises() == null ? 0 : Cache().getExercises().length;
if (sizeExerciseList == 0) {
diff --git a/lib/widgets/dialog_premium.dart b/lib/widgets/dialog_premium.dart
index 951e3f4..7c0eddc 100644
--- a/lib/widgets/dialog_premium.dart
+++ b/lib/widgets/dialog_premium.dart
@@ -160,7 +160,7 @@ class _DialogPremiumState extends State with Trans {
Align(
alignment: Alignment.center,
child: GestureDetector(
- onTap: widget.onTap ?? widget.onTap,
+ onTap: () => Navigator.of(context).pushNamed("salesPage"),
child: Stack(
alignment: Alignment.center,
children: [
diff --git a/lib/widgets/home.dart b/lib/widgets/home.dart
index 0a46a95..4826c1c 100644
--- a/lib/widgets/home.dart
+++ b/lib/widgets/home.dart
@@ -2,12 +2,14 @@ import 'package:aitrainer_app/bloc/session/session_bloc.dart';
import 'package:aitrainer_app/bloc/settings/settings_bloc.dart';
import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/service/logging.dart';
+import 'package:aitrainer_app/util/platform_purchase.dart';
import 'package:aitrainer_app/view/login.dart';
import 'package:aitrainer_app/view/menu_page.dart';
import 'package:aitrainer_app/view/registration.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
+import 'package:flutter/semantics.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'loading.dart';
@@ -85,4 +87,10 @@ class _HomePageState extends State with Logging {
}),
);
}
+
+ @override
+ void dispose() async {
+ super.dispose();
+ await PlatformPurchaseApi().close();
+ }
}
diff --git a/lib/widgets/image_button.dart b/lib/widgets/image_button.dart
index f34dbdd..0ea40a7 100644
--- a/lib/widgets/image_button.dart
+++ b/lib/widgets/image_button.dart
@@ -53,6 +53,8 @@ class ImageButton extends StatelessWidget {
top = height - (style.fontSize - 5) * text.length - 2 * left < 0 ? height - 2 * style.fontSize - 22 : height - style.fontSize - 37;
//print("Top: " + top.toStringAsFixed(0) + " length: " + ((style.fontSize - 5) * text.length).toString());
}
+ final double width = MediaQuery.of(context).size.width;
+ print("Mediawidth: " + width.toStringAsFixed(0));
return Stack(alignment: AlignmentDirectional.bottomStart, children: [
FlatButton(
child: image == null
@@ -101,29 +103,12 @@ class ImageButton extends StatelessWidget {
],
)),
)),
- /* Positioned(
- top: top,
- left: left,
- child: Container(
- height: width - 2 * left,
- width: width - 2 * left,
- child: InkWell(
- onTap: onTap ?? onTap,
- child: Text(
- text,
- maxLines: 2,
- style: style,
- ),
- ),
- color: Colors.transparent,
- ),
- ), */
Cache().hasPurchased
? Offstage()
: Stack(alignment: Alignment.topCenter, children: [
Positioned(
- top: 20,
- left: 20,
+ top: 10,
+ left: (width / 2 - 30) / 2 - 75,
child: this.isLocked
? GestureDetector(
child: Image.asset(
diff --git a/lib/widgets/menu_page_widget.dart b/lib/widgets/menu_page_widget.dart
index bc12ebb..69c2685 100644
--- a/lib/widgets/menu_page_widget.dart
+++ b/lib/widgets/menu_page_widget.dart
@@ -8,12 +8,14 @@ import 'package:aitrainer_app/localization/app_localization.dart';
import 'package:aitrainer_app/model/cache.dart';
import 'package:aitrainer_app/model/workout_menu_tree.dart';
import 'package:aitrainer_app/service/logging.dart';
+import 'package:aitrainer_app/util/not_found_exception.dart';
import 'package:aitrainer_app/util/trans.dart';
import 'package:aitrainer_app/widgets/dialog_common.dart';
import 'package:badges/badges.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -331,30 +333,30 @@ class _MenuPageWidgetState extends State with Trans, Logging {
return returnCode;
}
- dynamic _getButtonImage(WorkoutMenuTree workoutTree, double cWidth, double cHeight) {
- dynamic image;
- try {
+ Widget _getButtonImage(WorkoutMenuTree workoutTree, double cWidth, double cHeight) {
+ Widget image;
+
+ if (workoutTree.imageName.startsWith('https')) {
image = ClipRRect(
- borderRadius: BorderRadius.circular(24),
- child: Image.asset(
- workoutTree.imageName,
- height: 210,
- errorBuilder: (context, error, stackTrace) {
- String url = Cache.mediaUrl + 'images/' + workoutTree.imageName.substring(11);
- Widget image = FadeInImage.assetNetwork(
- placeholder: 'asset/image/dots.gif',
- image: url,
- height: 210,
- );
- return image;
- },
+ borderRadius: BorderRadius.circular(24.0),
+ child: Container(
+ color: Colors.black87,
+ child: FadeInImage(
+ image: NetworkImage(workoutTree.imageName),
+ placeholder: AssetImage("asset/image/dots.gif"),
+ ),
));
- } on Exception catch (_) {
- String url = Cache.mediaUrl + '/images/' + workoutTree.imageName;
- image = FadeInImage.assetNetwork(
- placeholder: 'asset/image/dots.gif',
- image: url,
- height: 210,
+ } else {
+ image = Container(
+ //width: cWidth - 30,
+ //height: 210.0,
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ fit: BoxFit.cover,
+ image: AssetImage(workoutTree.imageName),
+ ),
+ borderRadius: BorderRadius.all(Radius.circular(24.0)),
+ ),
);
}
@@ -365,6 +367,7 @@ class _MenuPageWidgetState extends State with Trans, Logging {
String badgeKey = workoutMenuTree.nameEnglish;
bool show = Cache().getBadges()[badgeKey] != null;
int counter = Cache().getBadges()[badgeKey] != null ? Cache().getBadges()[badgeKey] : 0;
+ Widget buttonImage = _getButtonImage(workoutMenuTree, cWidth, cHeight);
return Badge(
padding: EdgeInsets.all(8),
position: BadgePosition.topEnd(top: 3, end: 3),
@@ -377,7 +380,11 @@ class _MenuPageWidgetState extends State with Trans, Logging {
color: Colors.white,
fontSize: 16,
)),
- child: _getButtonImage(workoutMenuTree, cWidth, cHeight),
+ child: buttonImage == null
+ ? Container(
+ color: Colors.red,
+ )
+ : buttonImage,
);
}
diff --git a/pubspec.lock b/pubspec.lock
index 67b11f2..25edc20 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -369,7 +369,21 @@ packages:
name: flutter_facebook_auth
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.2+2"
+ version: "2.0.0+1"
+ flutter_facebook_auth_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_facebook_auth_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ flutter_facebook_auth_web:
+ dependency: transitive
+ description:
+ name: flutter_facebook_auth_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.6"
flutter_form_bloc:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index f4720b9..961a9e3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 1.1.3+44
+version: 1.1.4+45
environment:
sdk: ">=2.7.0 <3.0.0"
@@ -55,7 +55,7 @@ dependencies:
#firebase_analytics: ^6.2.0
firebase_messaging: ^7.0.3
firebase_auth: ^0.18.3
- flutter_facebook_auth: ^1.0.2+2
+ flutter_facebook_auth: ^2.0.0+1
flurry: ^0.0.7
animated_widgets: ^1.0.6