Merge pull request #5467 from vector-im/doug/5160_ftue_use_case

Add the FTUE use case screen for new users.
This commit is contained in:
Doug 2022-02-10 14:28:28 +00:00 committed by GitHub
commit 3943b17d57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1284 additions and 168 deletions

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_community.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#368BD6" fill-opacity="0.15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 29C24.5228 29 29 24.5228 29 19C29 13.4772 24.5228 9 19 9C13.4772 9 9 13.4772 9 19C9 24.5228 13.4772 29 19 29ZM25.3518 22.3106L24.5865 24.6722C24.5747 24.7086 24.5546 24.7419 24.5279 24.7694L23.7731 25.5458L22.9776 26.364L22.4305 26.9267C22.3097 27.051 22.1022 27.0176 22.0264 26.8617L21.4043 25.5819C21.3927 25.558 21.3773 25.5361 21.3587 25.517L20.5913 24.7276L19.8694 23.9852C19.8224 23.9367 19.7577 23.9094 19.6902 23.9094H18.3613C18.2656 23.9094 18.1783 23.8548 18.1365 23.7687L17.4346 22.3248C17.418 22.2908 17.4094 22.2534 17.4094 22.2155V19.9711C17.4094 19.8774 17.3571 19.7916 17.2738 19.7487L15.8724 19.028C15.837 19.0098 15.7978 19.0003 15.758 19.0003H14.3333C14.2657 19.0003 14.2011 18.973 14.154 18.9246L12.713 17.4424C12.6644 17.3924 12.6388 17.3244 12.6426 17.2547L12.7331 15.5685C12.7445 15.3571 12.7842 15.1482 12.8511 14.9474L13.1309 14.108C13.6777 12.4677 14.9701 11.1839 16.614 10.6481L17.6003 10.3428C18.5075 10.062 19.4825 10.0931 20.3699 10.4312L21.335 10.7988C21.3691 10.8118 21.3999 10.8321 21.4253 10.8582L22.8082 12.2806C22.9025 12.3776 22.9025 12.5321 22.8082 12.6291L20.662 14.8367C20.6166 14.8833 20.5913 14.9458 20.5913 15.0109V16.2391C20.5913 16.3974 20.446 16.5159 20.2909 16.484L16.9144 15.7894C16.7593 15.7575 16.614 15.8759 16.614 16.0343V17.114C16.614 17.252 16.7259 17.364 16.864 17.364H18.7504C18.8884 17.364 19.0004 17.4759 19.0004 17.614V18.7503C19.0004 18.8884 19.1123 19.0003 19.2504 19.0003H22.0766C22.1441 19.0003 22.2087 19.0276 22.2558 19.0761L22.9491 19.7892C22.968 19.8086 22.9899 19.8248 23.014 19.8372L24.5321 20.618C24.5562 20.6304 24.5782 20.6466 24.5971 20.666L25.2932 21.3821C25.3386 21.4288 25.364 21.4913 25.364 21.5564V22.2336C25.364 22.2597 25.3599 22.2857 25.3518 22.3106Z" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_community_dark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#368BD6" fill-opacity="0.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 29C24.5228 29 29 24.5228 29 19C29 13.4772 24.5228 9 19 9C13.4772 9 9 13.4772 9 19C9 24.5228 13.4772 29 19 29ZM25.3518 22.3106L24.5865 24.6722C24.5747 24.7086 24.5546 24.7419 24.5279 24.7694L23.7731 25.5458L22.9776 26.364L22.4305 26.9267C22.3097 27.051 22.1022 27.0176 22.0264 26.8617L21.4043 25.5819C21.3927 25.558 21.3773 25.5361 21.3587 25.517L20.5913 24.7276L19.8694 23.9852C19.8224 23.9367 19.7577 23.9094 19.6902 23.9094H18.3613C18.2656 23.9094 18.1783 23.8548 18.1365 23.7687L17.4346 22.3248C17.418 22.2908 17.4094 22.2534 17.4094 22.2155V19.9711C17.4094 19.8774 17.3571 19.7916 17.2738 19.7487L15.8724 19.028C15.837 19.0098 15.7978 19.0003 15.758 19.0003H14.3333C14.2657 19.0003 14.2011 18.973 14.154 18.9246L12.713 17.4424C12.6644 17.3924 12.6388 17.3244 12.6426 17.2547L12.7331 15.5685C12.7445 15.3571 12.7842 15.1482 12.8511 14.9474L13.1309 14.108C13.6777 12.4677 14.9701 11.1839 16.614 10.6481L17.6003 10.3428C18.5075 10.062 19.4825 10.0931 20.3699 10.4312L21.335 10.7988C21.3691 10.8118 21.3999 10.8321 21.4253 10.8582L22.8082 12.2806C22.9025 12.3776 22.9025 12.5321 22.8082 12.6291L20.662 14.8367C20.6166 14.8833 20.5913 14.9458 20.5913 15.0109V16.2391C20.5913 16.3974 20.446 16.5159 20.2909 16.484L16.9144 15.7894C16.7593 15.7575 16.614 15.8759 16.614 16.0343V17.114C16.614 17.252 16.7259 17.364 16.864 17.364H18.7504C18.8884 17.364 19.0004 17.4759 19.0004 17.614V18.7503C19.0004 18.8884 19.1123 19.0003 19.2504 19.0003H22.0766C22.1441 19.0003 22.2087 19.0276 22.2558 19.0761L22.9491 19.7892C22.968 19.8086 22.9899 19.8248 23.014 19.8372L24.5321 20.618C24.5562 20.6304 24.5782 20.6466 24.5971 20.666L25.2932 21.3821C25.3386 21.4288 25.364 21.4913 25.364 21.5564V22.2336C25.364 22.2597 25.3599 22.2857 25.3518 22.3106Z" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_icon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,4 @@
<svg width="71" height="70" viewBox="0 0 71 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M36 70C55.33 70 71 54.33 71 35C71 15.67 55.33 0 36 0C16.67 0 1 15.67 1 35C1 54.33 16.67 70 36 70Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.1655 26.3399C45.1655 34.8426 38.2529 41.7356 29.7259 41.7356C27.3129 41.7356 25.0291 41.1835 22.9947 40.1992L17.4268 41.9342C15.3689 42.5755 13.4328 40.6542 14.0666 38.6L15.7975 32.9908C14.8289 30.9776 14.2864 28.7219 14.2864 26.3399C14.2864 17.8372 21.1989 10.9443 29.7259 10.9443C38.2529 10.9443 45.1655 17.8372 45.1655 26.3399ZM33.1655 46.3665C31.1238 46.3665 29.175 45.9694 27.392 45.2478C29.984 50.0978 35.0976 53.3976 40.9822 53.3976C43.3876 53.3976 45.6641 52.8464 47.6923 51.8631L53.2379 53.5958C55.2953 54.2383 57.2329 52.3187 56.6003 50.264L54.8736 44.6536C55.8394 42.641 56.3804 40.3857 56.3804 38.0043C56.3804 32.1754 53.1396 27.1032 48.361 24.4902C49.4394 26.4605 50.062 28.6571 50.062 30.9731C50.062 39.4747 41.6697 46.3665 33.1655 46.3665Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_personal.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#AC3BA8" fill-opacity="0.15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.0668 19.0004C30.0668 25.0892 25.1308 30.0252 19.042 30.0252C17.026 30.0252 15.1365 29.4841 13.5106 28.5393L9.25692 29.852C8.47193 30.0942 7.73693 29.3572 7.98132 28.5729L9.33432 24.2306C8.49408 22.6744 8.01715 20.8931 8.01715 19.0004C8.01715 12.9116 12.9531 7.97559 19.042 7.97559C25.1308 7.97559 30.0668 12.9116 30.0668 19.0004ZM15.367 19.0004C15.367 19.6769 14.8186 20.2254 14.142 20.2254C13.4655 20.2254 12.9171 19.6769 12.9171 19.0004C12.9171 18.3239 13.4655 17.7754 14.142 17.7754C14.8186 17.7754 15.367 18.3239 15.367 19.0004ZM19.042 20.2254C19.7185 20.2254 20.2669 19.6769 20.2669 19.0004C20.2669 18.3239 19.7185 17.7754 19.042 17.7754C18.3654 17.7754 17.817 18.3239 17.817 19.0004C17.817 19.6769 18.3654 20.2254 19.042 20.2254ZM25.1668 19.0004C25.1668 19.6769 24.6184 20.2254 23.9419 20.2254C23.2653 20.2254 22.7169 19.6769 22.7169 19.0004C22.7169 18.3239 23.2653 17.7754 23.9419 17.7754C24.6184 17.7754 25.1668 18.3239 25.1668 19.0004Z" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_personal_dark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#AC3BA8" fill-opacity="0.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.0666 19C30.0666 25.0889 25.1307 30.0248 19.0418 30.0248C17.0259 30.0248 15.1364 29.4838 13.5105 28.5389L9.2568 29.8516C8.47181 30.0938 7.73681 29.3568 7.9812 28.5725L9.3342 24.2302C8.49396 22.674 8.01702 20.8927 8.01702 19C8.01702 12.9112 12.953 7.97522 19.0418 7.97522C25.1307 7.97522 30.0666 12.9112 30.0666 19ZM15.3669 19C15.3669 19.6766 14.8185 20.225 14.1419 20.225C13.4654 20.225 12.9169 19.6766 12.9169 19C12.9169 18.3235 13.4654 17.775 14.1419 17.775C14.8185 17.775 15.3669 18.3235 15.3669 19ZM19.0418 20.225C19.7184 20.225 20.2668 19.6766 20.2668 19C20.2668 18.3235 19.7184 17.775 19.0418 17.775C18.3653 17.775 17.8169 18.3235 17.8169 19C17.8169 19.6766 18.3653 20.225 19.0418 20.225ZM25.1667 19C25.1667 19.6766 24.6183 20.225 23.9417 20.225C23.2652 20.225 22.7168 19.6766 22.7168 19C22.7168 18.3235 23.2652 17.775 23.9417 17.775C24.6183 17.775 25.1667 18.3235 25.1667 19Z" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_work.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#0DBD8B" fill-opacity="0.15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 19C30 25.0751 25.0751 30 19 30C12.9249 30 8 25.0751 8 19C8 12.9249 12.9249 8 19 8C25.0751 8 30 12.9249 30 19ZM18.0835 16.4792C18.0835 18.1245 16.8522 19.4583 15.3335 19.4583C13.8147 19.4583 12.5835 18.1245 12.5835 16.4792C12.5835 14.8338 13.8147 13.5 15.3335 13.5C16.8522 13.5 18.0835 14.8338 18.0835 16.4792ZM26.2277 17.9433C26.2277 19.3848 25.1491 20.5533 23.8185 20.5533C22.488 20.5533 21.4093 19.3848 21.4093 17.9433C21.4093 16.5019 22.488 15.3334 23.8185 15.3334C25.1491 15.3334 26.2277 16.5019 26.2277 17.9433ZM19.9167 25.3207C19.9167 23.1735 18.4086 21.3786 16.3938 20.9373C16.0507 20.8691 15.6961 20.8334 15.3334 20.8334C13.4811 20.8334 11.8427 21.7648 10.846 23.1924C12.368 26.1464 15.4481 28.1667 19 28.1667C19.3094 28.1667 19.6152 28.1513 19.9167 28.1214L19.9167 25.3207ZM27.1779 23.1456C26.0706 25.3255 24.1173 27.0034 21.7502 27.747L21.7502 25.3207C21.7502 24.3428 21.5281 23.4167 21.1316 22.5902C21.8992 22.0646 22.8237 21.7579 23.8186 21.7579C25.125 21.7579 26.31 22.2868 27.1779 23.1456Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "onboarding_use_case_work_dark.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View file

@ -0,0 +1,5 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="white"/>
<path d="M0 14C0 6.26801 6.26801 0 14 0H24C31.732 0 38 6.26801 38 14V24C38 31.732 31.732 38 24 38H14C6.26801 38 0 31.732 0 24V14Z" fill="#0DBD8B" fill-opacity="0.3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30 19C30 25.0751 25.0751 30 19 30C12.9249 30 8 25.0751 8 19C8 12.9249 12.9249 8 19 8C25.0751 8 30 12.9249 30 19ZM18.0835 16.4792C18.0835 18.1245 16.8522 19.4583 15.3335 19.4583C13.8147 19.4583 12.5835 18.1245 12.5835 16.4792C12.5835 14.8338 13.8147 13.5 15.3335 13.5C16.8522 13.5 18.0835 14.8338 18.0835 16.4792ZM26.2277 17.9433C26.2277 19.3848 25.1491 20.5533 23.8185 20.5533C22.488 20.5533 21.4093 19.3848 21.4093 17.9433C21.4093 16.5019 22.488 15.3334 23.8185 15.3334C25.1491 15.3334 26.2277 16.5019 26.2277 17.9433ZM19.9167 25.3207C19.9167 23.1735 18.4086 21.3786 16.3938 20.9373C16.0507 20.8691 15.6961 20.8334 15.3334 20.8334C13.4811 20.8334 11.8427 21.7648 10.846 23.1924C12.368 26.1464 15.4481 28.1667 19 28.1667C19.3094 28.1667 19.6152 28.1513 19.9167 28.1214L19.9167 25.3207ZM27.1779 23.1456C26.0706 25.3255 24.1173 27.0034 21.7502 27.747L21.7502 25.3207C21.7502 24.3428 21.5281 23.4167 21.1316 22.5902C21.8992 22.0646 22.8237 21.7579 23.8186 21.7579C25.125 21.7579 26.31 22.2868 27.1779 23.1456Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -93,6 +93,17 @@
"onboarding_splash_page_4_title_no_pun" = "Messaging for your team.";
"onboarding_splash_page_4_message" = "Element is also great for the workplace. Its trusted by the worlds most secure organisations.";
"onboarding_use_case_title" = "Who will you chat to the most?";
"onboarding_use_case_message" = "Well help you get connected.";
"onboarding_use_case_personal_messaging" = "Friends and family";
"onboarding_use_case_work_messaging" = "Teams";
"onboarding_use_case_community_messaging" = "Communities";
/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */
"onboarding_use_case_not_sure_yet" = "Not sure yet? You can %@";
"onboarding_use_case_skip_button" = "skip this question";
"onboarding_use_case_existing_server_message" = "Looking to join an existing server?";
"onboarding_use_case_existing_server_button" = "Connect to server";
// Authentication
"auth_login" = "Log in";
"auth_register" = "Register";

View file

@ -118,6 +118,13 @@ internal enum Asset {
internal static let onboardingSplashScreenPage3Dark = ImageAsset(name: "OnboardingSplashScreenPage3Dark")
internal static let onboardingSplashScreenPage4 = ImageAsset(name: "OnboardingSplashScreenPage4")
internal static let onboardingSplashScreenPage4Dark = ImageAsset(name: "OnboardingSplashScreenPage4Dark")
internal static let onboardingUseCaseCommunity = ImageAsset(name: "onboarding_use_case_community")
internal static let onboardingUseCaseCommunityDark = ImageAsset(name: "onboarding_use_case_community_dark")
internal static let onboardingUseCaseIcon = ImageAsset(name: "onboarding_use_case_icon")
internal static let onboardingUseCasePersonal = ImageAsset(name: "onboarding_use_case_personal")
internal static let onboardingUseCasePersonalDark = ImageAsset(name: "onboarding_use_case_personal_dark")
internal static let onboardingUseCaseWork = ImageAsset(name: "onboarding_use_case_work")
internal static let onboardingUseCaseWorkDark = ImageAsset(name: "onboarding_use_case_work_dark")
internal static let peopleEmptyScreenArtwork = ImageAsset(name: "people_empty_screen_artwork")
internal static let peopleEmptyScreenArtworkDark = ImageAsset(name: "people_empty_screen_artwork_dark")
internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action")

View file

@ -2443,6 +2443,42 @@ public class VectorL10n: NSObject {
public static var onboardingSplashRegisterButtonTitle: String {
return VectorL10n.tr("Vector", "onboarding_splash_register_button_title")
}
/// Communities
public static var onboardingUseCaseCommunityMessaging: String {
return VectorL10n.tr("Vector", "onboarding_use_case_community_messaging")
}
/// Connect to server
public static var onboardingUseCaseExistingServerButton: String {
return VectorL10n.tr("Vector", "onboarding_use_case_existing_server_button")
}
/// Looking to join an existing server?
public static var onboardingUseCaseExistingServerMessage: String {
return VectorL10n.tr("Vector", "onboarding_use_case_existing_server_message")
}
/// Well help you get connected.
public static var onboardingUseCaseMessage: String {
return VectorL10n.tr("Vector", "onboarding_use_case_message")
}
/// Not sure yet? You can %@
public static func onboardingUseCaseNotSureYet(_ p1: String) -> String {
return VectorL10n.tr("Vector", "onboarding_use_case_not_sure_yet", p1)
}
/// Friends and family
public static var onboardingUseCasePersonalMessaging: String {
return VectorL10n.tr("Vector", "onboarding_use_case_personal_messaging")
}
/// skip this question
public static var onboardingUseCaseSkipButton: String {
return VectorL10n.tr("Vector", "onboarding_use_case_skip_button")
}
/// Who will you chat to the most?
public static var onboardingUseCaseTitle: String {
return VectorL10n.tr("Vector", "onboarding_use_case_title")
}
/// Teams
public static var onboardingUseCaseWorkMessaging: String {
return VectorL10n.tr("Vector", "onboarding_use_case_work_messaging")
}
/// Open
public static var `open`: String {
return VectorL10n.tr("Vector", "open")

View file

@ -99,7 +99,7 @@ final class RiotSettings: NSObject {
@UserDefault<String?>(key: "userInterfaceTheme", defaultValue: nil, storage: defaults)
var userInterfaceTheme
// MARK: Other
// MARK: Analytics & Rageshakes
/// Whether the user was previously shown the Matomo analytics prompt.
var hasSeenAnalyticsPrompt: Bool {
@ -130,6 +130,12 @@ final class RiotSettings: NSObject {
@UserDefault(key: "enableRageShake", defaultValue: false, storage: defaults)
var enableRageShake
// MARK: User
/// A dictionary of dictionaries keyed by user ID for storage of the `UserSessionProperties` from any active `UserSession`s.
@UserDefault(key: "userSessionProperties", defaultValue: [:], storage: defaults)
var userSessionProperties: [String: [String: Any]]
// MARK: Labs
/// Indicates if CallKit ringing is enabled for group calls. This setting does not disable the CallKit integration for group calls, only relates to ringing.

View file

@ -112,11 +112,18 @@ import DesignKit
/// - Parameter tabBar: The tab bar to customise.
func applyStyle(onTabBar tabBar: UITabBar)
/// Apply the theme on a navigation bar.
/// Apply the theme on a navigation bar, without enabling the iOS 15's scroll edges appearance.
///
/// - Parameter navigationBar: the navigation bar to customise.
func applyStyle(onNavigationBar navigationBar: UINavigationBar)
/// Apply the theme on a navigation bar.
///
/// - Parameter navigationBar: the navigation bar to customise.
/// - Parameter modernScrollEdgesAppearance: whether or not to use the iOS 15 style scroll edges appearance
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool)
/// Apply the theme on a search bar.
///
/// - Parameter searchBar: the search bar to customise.

View file

@ -112,28 +112,36 @@ class DarkTheme: NSObject, Theme {
}
}
// Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop.
// Protocols don't support default parameter values and a protocol extension won't work for @objc
func applyStyle(onNavigationBar navigationBar: UINavigationBar) {
navigationBar.tintColor = self.tintColor
applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false)
}
// Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop.
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) {
navigationBar.tintColor = tintColor
// On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style.
if #available(iOS 15.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = self.baseColor
appearance.backgroundColor = baseColor
if !modernScrollEdgesAppearance {
appearance.shadowColor = nil
}
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: self.textPrimaryColor
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: self.textPrimaryColor
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
navigationBar.barTintColor = self.baseColor
navigationBar.barTintColor = baseColor
navigationBar.shadowImage = UIImage() // Remove bottom shadow
// The navigation bar needs to be opaque so that its background color is the expected one

View file

@ -118,9 +118,15 @@ class DefaultTheme: NSObject, Theme {
}
}
// Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop.
// Protocols don't support default parameter values and a protocol extension doesn't work for @objc
func applyStyle(onNavigationBar navigationBar: UINavigationBar) {
navigationBar.tintColor = self.tintColor
applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: false)
}
// Note: We are not using UINavigationBarAppearance on iOS 13/14 because of UINavigationBar directly including UISearchBar on their titleView that cause crop issues with UINavigationController pop.
func applyStyle(onNavigationBar navigationBar: UINavigationBar,
withModernScrollEdgesAppearance modernScrollEdgesAppearance: Bool) {
navigationBar.tintColor = tintColor
// On iOS 15 use UINavigationBarAppearance to fix visual issues with the scrollEdgeAppearance style.
if #available(iOS 15.0, *) {
@ -128,13 +134,15 @@ class DefaultTheme: NSObject, Theme {
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = baseColor
if !modernScrollEdgesAppearance {
appearance.shadowColor = nil
}
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor
]
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.scrollEdgeAppearance = modernScrollEdgesAppearance ? nil : appearance
} else {
navigationBar.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textPrimaryColor

View file

@ -33,19 +33,21 @@ class UserSession: NSObject, UserSessionProtocol {
let account: MXKAccount
// Keep strong reference to the MXSession because account.mxSession can become nil on logout or failure
let matrixSession: MXSession
var userId: String {
guard let userId = self.account.mxCredentials.userId else {
fatalError("[UserSession] identifier: account.mxCredentials.userId should be defined")
}
return userId
}
let userId: String
/// An object that contains user specific properties.
let userProperties: UserSessionProperties
// MARK: - Setup
init(account: MXKAccount, matrixSession: MXSession) {
guard let userId = account.mxCredentials.userId else {
fatalError("[UserSession] identifier: account.mxCredentials.userId should be defined")
}
self.account = account
self.matrixSession = matrixSession
self.userId = userId
self.userProperties = UserSessionProperties(userId: userId)
super.init()
}
}

View file

@ -0,0 +1,86 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// User properties that are tied to a particular user ID.
class UserSessionProperties: NSObject {
// MARK: - Constants
private enum Constants {
static let useCaseKey = "useCase"
}
// MARK: - Properties
// MARK: Private
/// The user ID for these properties
private let userId: String
/// The underlying dictionary for this userId from user defaults.
private var dictionary: [String: Any] {
get {
RiotSettings.shared.userSessionProperties[userId] ?? [:]
}
set {
var sharedProperties = RiotSettings.shared.userSessionProperties
sharedProperties[userId] = newValue
RiotSettings.shared.userSessionProperties = sharedProperties
}
}
// MARK: Public
/// The user's use case selection if this session was the one used to register the account.
var useCase: UseCase? {
get {
guard let useCaseRawValue = dictionary[Constants.useCaseKey] as? String else { return nil }
return UseCase(rawValue: useCaseRawValue)
} set {
dictionary[Constants.useCaseKey] = newValue?.rawValue
}
}
/// Represents a selected use case for the app.
/// Note: The raw string value is used for storage.
enum UseCase: String {
case personalMessaging
case workMessaging
case communityMessaging
case skipped
}
// MARK: - Setup
/// Create new properties for the specified user ID.
/// - Parameter userId: The user ID to load properties for.
init(userId: String) {
self.userId = userId
super.init()
}
// MARK: - Public
/// Clear all of the stored properties.
func delete() {
dictionary = [:]
var sharedProperties = RiotSettings.shared.userSessionProperties
sharedProperties[userId] = nil
RiotSettings.shared.userSessionProperties = sharedProperties
}
}

View file

@ -131,6 +131,9 @@ class UserSessionsService: NSObject {
NotificationCenter.default.post(name: UserSessionsService.willRemoveUserSession, object: self, userInfo: [NotificationUserInfoKey.userSession: userSession])
}
// Clear any stored user properties from this session.
userSession.userProperties.delete()
self.userSessions.removeAll { (userSession) -> Bool in
return userId == userSession.userId
}

View file

@ -32,7 +32,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: (() -> Void)?
var completion: ((MXKAuthenticationType) -> Void)?
// MARK: - Setup
@ -62,6 +62,10 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
authenticationViewController.authType = authenticationType
}
func showCustomServer() {
authenticationViewController.setCustomServerFieldsVisible(true)
}
func update(externalRegistrationParameters: [AnyHashable: Any]) {
authenticationViewController.externalRegistrationParameters = externalRegistrationParameters
}
@ -82,6 +86,6 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
// MARK: - AuthenticationViewControllerDelegate
extension AuthenticationCoordinator: AuthenticationViewControllerDelegate {
func authenticationViewControllerDidDismiss(_ authenticationViewController: AuthenticationViewController!) {
completion?()
completion?(authenticationViewController.authType)
}
}

View file

@ -20,11 +20,14 @@ import Foundation
/// `AuthenticationCoordinatorProtocol` is a protocol describing a Coordinator that handle's the authentication navigation flow.
protocol AuthenticationCoordinatorProtocol: Coordinator, Presentable {
var completion: (() -> Void)? { get set }
var completion: ((MXKAuthenticationType) -> Void)? { get set }
/// Update the screen to display registration or login.
func update(authenticationType: MXKAuthenticationType)
/// Enable the custom server checkbox to allow the user to enter a homeserver URL.
func showCustomServer()
/// Force a registration process based on a predefined set of parameters from a server provisioning link.
/// For more information see `AuthenticationViewController.externalRegistrationParameters`.
func update(externalRegistrationParameters: [AnyHashable: Any])

View file

@ -53,6 +53,10 @@
/// returns YES if the SSO login can be continued.
- (BOOL)continueSSOLoginWithToken:(NSString*)loginToken txnId:(NSString*)txnId;
/// Show or hide the custom server textfields.
/// @param isVisible YES to show, NO to hide.
- (void)setCustomServerFieldsVisible:(BOOL)isVisible;
@end

View file

@ -160,7 +160,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
}
self.serverOptionsContainer.hidden = !BuildSettings.authScreenShowCustomServerOptions;
[self hideCustomServers:YES];
[self setCustomServerFieldsVisible:NO];
// Soft logout section
self.softLogoutClearDataButton.layer.cornerRadius = 5;
@ -214,7 +214,8 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
- (void)userInterfaceThemeDidChange
{
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar];
[ThemeService.shared.theme applyStyleOnNavigationBar:self.navigationController.navigationBar
withModernScrollEdgesAppearance:YES];
self.view.backgroundColor = ThemeService.shared.theme.backgroundColor;
@ -887,7 +888,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
{
if (sender == self.customServersTickButton)
{
[self hideCustomServers:!self.customServersContainer.hidden];
[self setCustomServerFieldsVisible:self.customServersContainer.hidden];
}
else if (sender == self.forgotPasswordButton)
{
@ -1235,14 +1236,14 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
[self.view layoutIfNeeded];
}
- (void)hideCustomServers:(BOOL)hidden
- (void)setCustomServerFieldsVisible:(BOOL)isVisible
{
if (self.customServersContainer.isHidden == hidden)
if (self.customServersContainer.isHidden != isVisible)
{
return;
}
if (hidden)
if (!isVisible)
{
[self.homeServerTextField resignFirstResponder];
[self.identityServerTextField resignFirstResponder];
@ -1360,7 +1361,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
[self.authenticationActivityIndicator startAnimating];
// Hide the custom server details in order to save customized inputs
[self hideCustomServers:YES];
[self setCustomServerFieldsVisible:NO];
MXKAccount *account = [[MXKAccountManager sharedManager] accountForUserId:userId];
MXSession *session = account.mxSession;
@ -1585,7 +1586,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
{
// wellKnown matches with application default servers
// Hide custom servers
[self hideCustomServers:YES];
[self setCustomServerFieldsVisible:NO];
}
else
{
@ -1617,7 +1618,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0;
}
// And show custom servers
[self hideCustomServers:NO];
[self setCustomServerFieldsVisible:YES];
}
#pragma mark - KeyVerificationCoordinatorBridgePresenterDelegate

View file

@ -28,6 +28,10 @@ class VectorHostingController: UIHostingController<AnyView> {
private var theme: Theme
// MARK: Public
var enableNavigationBarScrollEdgesAppearance = false
init<Content>(rootView: Content) where Content: View {
self.theme = ThemeService.shared().theme
super.init(rootView: AnyView(rootView.vectorContent()))
@ -67,7 +71,7 @@ class VectorHostingController: UIHostingController<AnyView> {
private func update(theme: Theme) {
if let navigationBar = self.navigationController?.navigationBar {
theme.applyStyle(onNavigationBar: navigationBar)
theme.applyStyle(onNavigationBar: navigationBar, withModernScrollEdgesAppearance: enableNavigationBarScrollEdgesAppearance)
}
}
}

View file

@ -60,6 +60,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
// MARK: Screen results
private var splashScreenResult: OnboardingSplashScreenViewModelResult?
private var useCaseResult: OnboardingUseCaseViewModelResult?
// MARK: Public
@ -126,9 +127,47 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
}
@available(iOS 14.0, *)
/// Displays the next view in the flow after the splash screen.
private func splashScreenCoordinator(_ coordinator: OnboardingSplashScreenCoordinator, didCompleteWith result: OnboardingSplashScreenViewModelResult) {
splashScreenResult = result
// Set the auth type early to allow network requests to finish during display of the use case screen.
let mxkAuthenticationType = splashScreenResult == .register ? MXKAuthenticationTypeRegister : MXKAuthenticationTypeLogin
authenticationCoordinator.update(authenticationType: mxkAuthenticationType)
switch result {
case .register:
showUseCaseSelectionScreen()
case .login:
showAuthenticationScreen()
}
}
@available(iOS 14.0, *)
/// Show the use case screen for new users.
private func showUseCaseSelectionScreen() {
let coordinator = OnboardingUseCaseCoordinator()
coordinator.completion = { [weak self, weak coordinator] result in
guard let self = self, let coordinator = coordinator else { return }
self.useCaseCoordinator(coordinator, didCompleteWith: result)
}
coordinator.start()
add(childCoordinator: coordinator)
if self.navigationRouter.modules.isEmpty {
self.navigationRouter.setRootModule(coordinator, popCompletion: nil)
} else {
self.navigationRouter.push(coordinator, animated: true) { [weak self] in
self?.remove(childCoordinator: coordinator)
}
}
}
/// Displays the next view in the flow after the use case screen.
private func useCaseCoordinator(_ coordinator: OnboardingUseCaseCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
useCaseResult = result
showAuthenticationScreen()
}
@ -139,21 +178,22 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
MXLog.debug("[OnboardingCoordinator] showAuthenticationScreen")
let coordinator = authenticationCoordinator
coordinator.completion = { [weak self, weak coordinator] in
coordinator.completion = { [weak self, weak coordinator] authenticationType in
guard let self = self, let coordinator = coordinator else { return }
self.authenticationCoordinatorDidComplete(coordinator)
self.authenticationCoordinator(coordinator, didCompleteWith: authenticationType)
}
// Due to needing to preload the authVC, this breaks the Coordinator init/start pattern.
// This can be re-assessed once we re-write a native flow for authentication.
// Set authType first as registration parameters or soft logout credentials will modify this.
let mxkAuthenticationType = splashScreenResult == .register ? MXKAuthenticationTypeRegister : MXKAuthenticationTypeLogin
coordinator.update(authenticationType: mxkAuthenticationType)
if let externalRegistrationParameters = externalRegistrationParameters {
coordinator.update(externalRegistrationParameters: externalRegistrationParameters)
}
if useCaseResult == .customServer {
coordinator.showCustomServer()
}
if let softLogoutCredentials = parameters.softLogoutCredentials {
coordinator.update(softLogoutCredentials: softLogoutCredentials)
}
@ -178,8 +218,34 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
}
/// Displays the next view in the flow after the authentication screen.
private func authenticationCoordinatorDidComplete(_ coordinator: AuthenticationCoordinatorProtocol) {
private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didCompleteWith authenticationType: MXKAuthenticationType) {
completion?()
isShowingAuthentication = false
// Handle the chosen use case if appropriate
if authenticationType == MXKAuthenticationTypeRegister,
let useCaseResult = useCaseResult,
let userSession = UserSessionsService.shared.mainUserSession {
// Store the value in the user's session
userSession.userProperties.useCase = useCaseResult.userSessionPropertyValue
}
}
}
extension OnboardingUseCaseViewModelResult {
/// The result converted into the type stored in the user session.
var userSessionPropertyValue: UserSessionProperties.UseCase? {
switch self {
case .personalMessaging:
return .personalMessaging
case .workMessaging:
return .workMessaging
case .communityMessaging:
return .communityMessaging
case .skipped:
return .skipped
case .customServer:
return nil
}
}
}

View file

@ -63,6 +63,7 @@ targets:
- path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift
- path: ../Riot/Modules/MatrixKit
- path: ../Riot/Modules/Analytics
- path: ../Riot/Managers/UserSessions
- path: ../Riot/Managers/AppInfo/
excludes:
- "**/*.md" # excludes all files with the .md extension

View file

@ -70,6 +70,7 @@ targets:
buildPhase: resources
- path: ../Riot/Modules/MatrixKit
- path: ../Riot/Modules/Analytics
- path: ../Riot/Managers/UserSessions
excludes:
- "**/*.md" # excludes all files with the .md extension
- path: ../Riot/Generated/MatrixKitStrings.swift

View file

@ -42,18 +42,13 @@ struct AnalyticsPromptViewState: BindableState {
/// A collection of strings for the UI that need to be created in
/// the coordinator or mocked in the RiotSwiftUI target.
protocol AnalyticsPromptStringsProtocol {
var appDisplayName: String { get }
var point1: NSAttributedString { get }
var point2: NSAttributedString { get }
var termsNewUser: NSAttributedString { get }
var termsUpgrade: NSAttributedString { get }
}
enum AnalyticsPromptType {
case newUser(termsString: NSAttributedString)
case upgrade(termsString: NSAttributedString)
case newUser
case upgrade
}
extension AnalyticsPromptType {
@ -67,11 +62,23 @@ extension AnalyticsPromptType {
}
}
/// The terms string that should be displayed.
var termsStrings: NSAttributedString {
/// The main part of the terms string that should be displayed.
var mainTermsString: String {
switch self {
case .newUser(let termsString), .upgrade(let termsString):
return termsString
case .newUser:
return VectorL10n.analyticsPromptTermsNewUser("%@")
case .upgrade:
return VectorL10n.analyticsPromptTermsUpgrade("%@")
}
}
/// The tappable part of the terms string that should be displayed.
var termsLinkString: String {
switch self {
case .newUser:
return VectorL10n.analyticsPromptTermsLinkNewUser
case .upgrade:
return VectorL10n.analyticsPromptTermsLinkUpgrade
}
}
@ -96,15 +103,7 @@ extension AnalyticsPromptType {
}
}
extension AnalyticsPromptType: CaseIterable {
static var allCases: [AnalyticsPromptType] {
let strings = MockAnalyticsPromptStrings()
return [
.newUser(termsString: strings.termsNewUser),
.upgrade(termsString: strings.termsUpgrade)
]
}
}
extension AnalyticsPromptType: CaseIterable { }
extension AnalyticsPromptType: Identifiable {
var id: String {

View file

@ -52,9 +52,9 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable {
let promptType: AnalyticsPromptType
if Analytics.shared.promptShouldDisplayUpgradeMessage {
promptType = .upgrade(termsString: strings.termsUpgrade)
promptType = .upgrade
} else {
promptType = .newUser(termsString: strings.termsNewUser)
promptType = .newUser
}
let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings, termsURL: BuildSettings.analyticsTermsURL)

View file

@ -18,16 +18,7 @@ import Foundation
@available(iOS 14.0, *)
struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
let appDisplayName = AppInfo.current.displayName
let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize)
let termsNewUser = HTMLFormatter().format(VectorL10n.analyticsPromptTermsNewUser("%@"),
with: VectorL10n.analyticsPromptTermsLinkNewUser,
using: BuildSettings.analyticsTermsURL)
let termsUpgrade = HTMLFormatter().format(VectorL10n.analyticsPromptTermsUpgrade("%@"),
with: VectorL10n.analyticsPromptTermsLinkUpgrade,
using: BuildSettings.analyticsTermsURL)
}

View file

@ -17,14 +17,9 @@
import UIKit
struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
var appDisplayName = "Element"
let point1: NSAttributedString
let point2: NSAttributedString
let termsNewUser: NSAttributedString
let termsUpgrade: NSAttributedString
let shortString = NSAttributedString(string: "This is a short string.")
let longString = NSAttributedString(string: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.")
@ -38,15 +33,5 @@ struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol {
point2.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)]))
point2.append(NSAttributedString(string: " share information with third parties"))
self.point2 = point2
let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ")
termsNewUser.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsNewUser.append(NSAttributedString(string: "."))
self.termsNewUser = termsNewUser
let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ")
termsUpgrade.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!]))
termsUpgrade.append(NSAttributedString(string: ". Is that OK?"))
self.termsUpgrade = termsUpgrade
}
}

View file

@ -42,10 +42,8 @@ struct AnalyticsPrompt: View {
VStack {
Text("\(viewModel.viewState.promptType.message)\n")
AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings)
.accessibilityLabel(Text(viewModel.viewState.promptType.termsStrings.string))
.accessibilityValue(Text(VectorL10n.accessibilityButtonLabel))
.onTapGesture {
InlineTextButton(viewModel.viewState.promptType.mainTermsString,
tappableText: viewModel.viewState.promptType.termsLinkString) {
viewModel.send(viewAction: .openTermsURL)
}
}
@ -71,7 +69,7 @@ struct AnalyticsPrompt: View {
Image(uiImage: Asset.Images.analyticsLogo.image)
.padding(.bottom, 25)
Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName))
Text(VectorL10n.analyticsPromptTitle(AppInfo.current.displayName))
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
.padding(.bottom, 2)
@ -125,6 +123,7 @@ struct AnalyticsPrompt: View {
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
}
}

View file

@ -1,74 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
/// The last line of text in the description with highlighting on the link string.
struct AnalyticsPromptTermsText: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
/// A string with a link attribute.
private struct StringComponent {
let string: String
let isLink: Bool
}
/// Internal representation of the string as composable parts.
private let components: [StringComponent]
// MARK: - Setup
init(attributedString: NSAttributedString) {
var components = [StringComponent]()
let range = NSRange(location: 0, length: attributedString.length)
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
let isLink = attributes.keys.contains(.link)
components.append(StringComponent(string: string.substring(with: range), isLink: isLink))
}
self.components = components
}
// MARK: - Views
var body: some View {
components.reduce(Text("")) {
$0 + Text($1.string).foregroundColor($1.isLink ? theme.colors.accent : nil)
}
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct AnalyticsPromptTermsText_Previews: PreviewProvider {
static let strings = MockAnalyticsPromptStrings()
static var previews: some View {
VStack(spacing: 8) {
AnalyticsPromptTermsText(attributedString: strings.termsNewUser)
AnalyticsPromptTermsText(attributedString: strings.termsUpgrade)
}
}
}

View file

@ -20,6 +20,7 @@ import Foundation
@available(iOS 14.0, *)
enum MockAppScreens {
static let appScreens: [MockScreenState.Type] = [
MockOnboardingUseCaseScreenState.self,
MockOnboardingSplashScreenScreenState.self,
MockLocationSharingScreenState.self,
MockAnalyticsPromptScreenState.self,

View file

@ -0,0 +1,89 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead that includes a link and handle the tap by adding an OpenURLAction to the environment.")
/// A `Button`, that fakes having a tappable string inside of a regular string.
struct InlineTextButton: View {
private struct StringComponent {
let string: Substring
let isTinted: Bool
}
// MARK: - Properties
// MARK: Private
/// The individual components of the string.
private let components: [StringComponent]
private let action: () -> Void
// MARK: - Setup
/// Creates a new `InlineTextButton`.
/// - Parameters:
/// - mainText: The main text that shouldn't appear tappable. This must contain a single `%@` placeholder somewhere within.
/// - tappableText: The tappable text that will be substituted into the `%@` placeholder.
/// - action: The action to perform when tapping the button.
internal init(_ mainText: String, tappableText: String, action: @escaping () -> Void) {
guard let range = mainText.range(of: "%@") else {
self.components = [StringComponent(string: Substring(mainText), isTinted: false)]
self.action = action
return
}
let firstComponent = StringComponent(string: mainText[..<range.lowerBound], isTinted: false)
let middleComponent = StringComponent(string: Substring(tappableText), isTinted: true)
let lastComponent = StringComponent(string: mainText[range.upperBound...], isTinted: false)
self.components = [firstComponent, middleComponent, lastComponent]
self.action = action
}
// MARK: - Views
var body: some View {
Button(action: action) {
EmptyView()
}
.buttonStyle(Style(components: components))
.accessibilityLabel(components.map { $0.string }.joined())
}
private struct Style: ButtonStyle {
let components: [StringComponent]
func makeBody(configuration: Configuration) -> some View {
components.reduce(Text("")) { lastValue, component in
lastValue + Text(component.string)
.foregroundColor(component.isTinted ? .accentColor.opacity(configuration.isPressed ? 0.2 : 1) : nil)
}
}
}
}
@available(iOS 14.0, *)
struct Previews_InlineButtonText_Previews: PreviewProvider {
static var previews: some View {
InlineTextButton("Hello there this is a sentence. %@.",
tappableText: "And this is a button",
action: { })
.padding()
}
}

View file

@ -0,0 +1,126 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
import DesignKit
@available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead.")
/// A `Text` view that renders attributed strings with their `.font` and `.foregroundColor` attributes.
/// This view is a workaround for iOS 13/14 not supporting `AttributedString`.
struct StyledText: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
/// A string with a bold property.
private struct StringComponent {
let string: String
var font: Font? = nil
var color: Color? = nil
}
/// Internal representation of the string as composable parts.
private let components: [StringComponent]
// MARK: - Setup
/// Creates a `StyledText` using the supplied attributed string.
/// - Parameter attributedString: The attributed string to display.
init(_ attributedString: NSAttributedString) {
var components = [StringComponent]()
let range = NSRange(location: 0, length: attributedString.length)
let string = attributedString.string as NSString
attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in
let font = attributes[.font] as? UIFont
let color = attributes[.foregroundColor] as? UIColor
let component = StringComponent(
string: string.substring(with: range),
font: font.map { Font($0) },
color: color.map { Color($0) }
)
components.append(component)
}
self.components = components
}
/// Creates a `StyledText` using a plain string.
/// - Parameter string: The plain string to display
init(_ string: String) {
self.components = [StringComponent(string: string, font: nil)]
}
// MARK: - Views
var body: some View {
components.reduce(Text("")) { lastValue, component in
lastValue + Text(component.string)
.font(component.font)
.foregroundColor(component.color)
}
}
}
@available(iOS 14.0, *)
struct StyledText_Previews: PreviewProvider {
static func prettyText() -> NSAttributedString {
let string = NSMutableAttributedString(string: "T", attributes: [
.font: UIFont.boldSystemFont(ofSize: 12),
.foregroundColor: UIColor.red
])
string.append(NSAttributedString(string: "e", attributes: [
.font: UIFont.boldSystemFont(ofSize: 14),
.foregroundColor: UIColor.orange
]))
string.append(NSAttributedString(string: "s", attributes: [
.font: UIFont.boldSystemFont(ofSize: 13),
.foregroundColor: UIColor.yellow
]))
string.append(NSAttributedString(string: "t", attributes: [
.font: UIFont.boldSystemFont(ofSize: 15),
.foregroundColor: UIColor.green
]))
string.append(NSAttributedString(string: "i", attributes: [
.font: UIFont.boldSystemFont(ofSize: 11),
.foregroundColor: UIColor.cyan
]))
string.append(NSAttributedString(string: "n", attributes: [
.font: UIFont.boldSystemFont(ofSize: 16),
.foregroundColor: UIColor.blue
]))
string.append(NSAttributedString(string: "g", attributes: [
.font: UIFont.boldSystemFont(ofSize: 14),
.foregroundColor: UIColor.purple
]))
return string
}
static var previews: some View {
VStack(spacing: 8) {
StyledText("Hello, World!")
StyledText(NSAttributedString(string: "Testing",
attributes: [.font: UIFont.boldSystemFont(ofSize: 64)]))
StyledText(prettyText())
}
}
}

View file

@ -0,0 +1,32 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct OnboardingButtonStyle: ButtonStyle {
@Environment(\.theme) private var theme
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(configuration.isPressed ? theme.colors.accent : theme.colors.quinaryContent, lineWidth: configuration.isPressed ? 2 : 1.5)
)
.contentShape(RoundedRectangle(cornerRadius: 8))
}
}

View file

@ -48,6 +48,7 @@ struct OnboardingSplashScreenPage: View {
.scaledToFit()
.frame(maxWidth: 300)
.padding(20)
.accessibilityHidden(true)
VStack(spacing: 8) {
OnboardingSplashScreenTitleText(content.title)

View file

@ -0,0 +1,61 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let onboardingUseCaseHostingController: UIViewController
private var onboardingUseCaseViewModel: OnboardingUseCaseViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((OnboardingUseCaseViewModelResult) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init() {
let viewModel = OnboardingUseCaseViewModel()
let view = OnboardingUseCase(viewModel: viewModel.context)
onboardingUseCaseViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.vc_removeBackTitle()
hostingController.enableNavigationBarScrollEdgesAppearance = true
onboardingUseCaseHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[OnboardingUseCaseCoordinator] did start.")
onboardingUseCaseViewModel.completion = { [weak self] result in
MXLog.debug("[OnboardingUseCaseCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
guard let self = self else { return }
self.completion?(result)
}
}
func toPresentable() -> UIViewController {
return self.onboardingUseCaseHostingController
}
}

View file

@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case `default`
/// The associated screen
var screenType: Any.Type {
OnboardingUseCase.self
}
/// A list of screen state definitions
static var allCases: [MockOnboardingUseCaseScreenState] {
// Each of the presence statuses
[.default]
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel = OnboardingUseCaseViewModel()
// can simulate service and viewModel actions here if needs be.
return (
[self, viewModel],
AnyView(OnboardingUseCase(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}

View file

@ -0,0 +1,41 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
// MARK: - Coordinator
// MARK: View model
enum OnboardingUseCaseStateAction {
case viewAction(OnboardingUseCaseViewAction)
}
enum OnboardingUseCaseViewModelResult {
case personalMessaging
case workMessaging
case communityMessaging
case skipped
case customServer
}
// MARK: View
struct OnboardingUseCaseViewState: BindableState { }
enum OnboardingUseCaseViewAction {
case answer(OnboardingUseCaseViewModelResult)
}

View file

@ -0,0 +1,52 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14, *)
typealias OnboardingUseCaseViewModelType = StateStoreViewModel<OnboardingUseCaseViewState,
OnboardingUseCaseStateAction,
OnboardingUseCaseViewAction>
@available(iOS 14, *)
class OnboardingUseCaseViewModel: OnboardingUseCaseViewModelType, OnboardingUseCaseViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((OnboardingUseCaseViewModelResult) -> Void)?
// MARK: - Setup
init() {
super.init(initialViewState: OnboardingUseCaseViewState())
}
// MARK: - Public
override func process(viewAction: OnboardingUseCaseViewAction) {
switch viewAction {
case .answer(let result):
completion?(result)
}
}
override class func reducer(state: inout OnboardingUseCaseViewState, action: OnboardingUseCaseStateAction) {
// There is no mutable state to reduce :)
}
}

View file

@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol OnboardingUseCaseViewModelProtocol {
var completion: ((OnboardingUseCaseViewModelResult) -> Void)? { get set }
@available(iOS 14, *)
var context: OnboardingUseCaseViewModelType.Context { get }
}

View file

@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingUseCaseUITests: MockScreenTest {
// The view has no parameters or changing state to test.
}

View file

@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingUseCaseViewModelTests: XCTestCase {
// The view model has nothing to test.
}

View file

@ -0,0 +1,130 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
/// The screen shown to a new user to select their use case for the app.
struct OnboardingUseCase: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme
// MARK: Public
@ObservedObject var viewModel: OnboardingUseCaseViewModel.Context
/// The screen's title and instructions.
var titleContent: some View {
VStack(spacing: 8) {
Image(Asset.Images.onboardingUseCaseIcon.name)
.padding(.bottom, 8)
.accessibilityHidden(true)
Text(VectorL10n.onboardingUseCaseTitle)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
Text(VectorL10n.onboardingUseCaseMessage)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
}
}
/// The buttons used to select a use case for the app.
var useCaseButtons: some View {
VStack(spacing: 8) {
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCasePersonalMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCasePersonalDark : Asset.Images.onboardingUseCasePersonal) {
viewModel.send(viewAction: .answer(.personalMessaging))
}
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseWorkMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCaseWorkDark : Asset.Images.onboardingUseCaseWork) {
viewModel.send(viewAction: .answer(.workMessaging))
}
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseCommunityMessaging,
image: theme.isDark ? Asset.Images.onboardingUseCaseCommunityDark : Asset.Images.onboardingUseCaseCommunity) {
viewModel.send(viewAction: .answer(.communityMessaging))
}
InlineTextButton(VectorL10n.onboardingUseCaseNotSureYet("%@"),
tappableText: VectorL10n.onboardingUseCaseSkipButton) {
viewModel.send(viewAction: .answer(.skipped))
}
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.tertiaryContent)
.padding(.top, 8)
}
}
/// A footer showing a button to connect to a server.
var serverFooter: some View {
VStack(spacing: 14) {
Text(VectorL10n.onboardingUseCaseExistingServerMessage)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.tertiaryContent)
Button { viewModel.send(viewAction: .answer(.customServer)) } label: {
Text(VectorL10n.onboardingUseCaseExistingServerButton)
.font(theme.fonts.body)
}
}
}
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
VStack(spacing: 0) {
titleContent
.padding(.bottom, 36)
useCaseButtons
}
.frame(maxWidth: OnboardingConstants.maxContentWidth,
maxHeight: OnboardingConstants.maxContentHeight)
.padding(16)
}
.frame(maxWidth: .infinity)
serverFooter
.padding(.horizontal, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
}
}
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct OnboardingUseCase_Previews: PreviewProvider {
static let stateRenderer = MockOnboardingUseCaseScreenState.stateRenderer
static var previews: some View {
NavigationView {
stateRenderer.screenGroup()
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

View file

@ -0,0 +1,59 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
/// A button used for the Use Case selection.
struct OnboardingUseCaseButton: View {
// MARK: Private
@Environment(\.theme) private var theme
// MARK: Public
/// The button's title.
let title: String
/// The button's image.
let image: ImageAsset
/// The button's action when tapped.
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
Image(image.name)
Text(title)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent)
}
.padding(16)
}
.buttonStyle(OnboardingButtonStyle())
}
}
@available(iOS 14.0, *)
struct Previews_OnboardingUseCaseButton_Previews: PreviewProvider {
static var previews: some View {
OnboardingUseCaseButton(title: VectorL10n.onboardingUseCaseWorkMessaging,
image: Asset.Images.onboardingUseCaseWork,
action: { })
.padding(16)
}
}

View file

@ -0,0 +1,102 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import Riot
class OnboardingTests: XCTestCase {
let userId = "@test:matrix.org"
override func setUp() {
// Clear any properties for the test
UserSessionProperties(userId: userId).delete()
}
func testEmptyUseCase() {
// Given an empty set of user properties
let properties = UserSessionProperties(userId: userId)
// Then the use case property should return nil
XCTAssertNil(properties.useCase, "A use case has not been set")
}
func testPersonalMessagingUseCase() {
// Given an empty set of user properties
let properties = UserSessionProperties(userId: userId)
// When storing a use case result of personal messaging
let result = OnboardingUseCaseViewModelResult.personalMessaging
properties.useCase = result.userSessionPropertyValue
// Then the use case property should return personal messaging
XCTAssertEqual(properties.useCase, .personalMessaging, "The use case should be Personal Messaging")
}
func testSkippedUseCase() {
// Given an empty set of user properties
let properties = UserSessionProperties(userId: userId)
// When storing a skipped use case result
let result = OnboardingUseCaseViewModelResult.skipped
properties.useCase = result.userSessionPropertyValue
// Then the use case property should return skipped
XCTAssertEqual(properties.useCase, .skipped)
}
func testCustomServerUseCase() {
// Given an empty set of user properties
let properties = UserSessionProperties(userId: userId)
// When storing a custom server case result
let result = OnboardingUseCaseViewModelResult.customServer
properties.useCase = result.userSessionPropertyValue
// Then the use case property should return nil
XCTAssertNil(properties.useCase)
}
func testUseCaseAfterDeletingProperties() {
// Given a set of user properties with the Work Messaging use case
let properties = UserSessionProperties(userId: userId)
let result = OnboardingUseCaseViewModelResult.workMessaging
properties.useCase = result.userSessionPropertyValue
XCTAssertEqual(properties.useCase, .workMessaging, "The use case should be Work Messaging")
// When deleting the user properties
properties.delete()
// Then the use case property should return nil
XCTAssertNil(properties.useCase)
}
func testUseCasePersistence() {
// Given a set of user properties with the Personal Messaging use case
var properties: UserSessionProperties? = UserSessionProperties(userId: userId)
let result = OnboardingUseCaseViewModelResult.personalMessaging
properties?.useCase = result.userSessionPropertyValue
XCTAssertEqual(properties?.useCase, .personalMessaging, "The use case should be Personal Messaging")
// When the app is relaunched and a new user properties instance is creates
properties = nil
let newProperties = UserSessionProperties(userId: userId)
// Then the use case property should still return Personal Messaging
XCTAssertEqual(newProperties.useCase, .personalMessaging, "The use case should be Personal Messaging")
}
}

View file

@ -52,6 +52,7 @@ targets:
- path: ../Riot/Managers/Locale/LocaleProvider.swift
- path: ../Riot/Modules/MatrixKit
- path: ../Riot/Modules/Analytics
- path: ../Riot/Managers/UserSessions
- path: ../Riot/Managers/AppInfo/
- path: ../Riot/Managers/Locale/LocaleProviderType.swift
- path: ../Riot/Generated/Strings.swift

1
changelog.d/5160.feature Normal file
View file

@ -0,0 +1 @@
Add Onboarding Use Case selection screen after the splash screen.