diff --git a/expo/assets/translations.json b/expo/assets/translations.json index 6577f86..87c0559 100644 --- a/expo/assets/translations.json +++ b/expo/assets/translations.json @@ -198,10 +198,22 @@ "ja": "Tシャツ" }, "Elineup Mall Settings": { - "ja": "イーラインナップモールの設定" + "ja": "Elineup!Mallの設定" }, - "Order End At": { - "ja": "販売終了日時" + "Fetch Elnieup Mall Purchase History": { + "ja": "Elineup!Mallの購入履歴を取得" + }, + "Ordered On": { + "ja": "注文日" + }, + "FC credentials will be used to fetch your purchase history from elineupmall.": { + "ja": "ファンクラブの認証情報を使用して、Elineup!Mallから購入履歴を取得します。" + }, + "Following settings per a member per a category": { + "ja": "メンバーごとのカテゴリごとのフォロー設定" + }, + "Order End On": { + "ja": "販売終了日" }, "Price": { "ja": "価格" diff --git a/expo/features/app/settings/internals/SettingsUserConfig.ts b/expo/features/app/settings/internals/SettingsUserConfig.ts index b2f438c..452ae41 100644 --- a/expo/features/app/settings/internals/SettingsUserConfig.ts +++ b/expo/features/app/settings/internals/SettingsUserConfig.ts @@ -12,6 +12,9 @@ export type UserConfig = { consentOnToS?: boolean; consentOnUPFCDataPolicy?: boolean; + elineupmallFetchPurchaseHistory: boolean; + + // deprecated amebloOptimizedView: boolean; feedUseMemberTaggings: boolean; @@ -27,6 +30,8 @@ export const SettingsUserConfigDefault: UserConfig = { consentOnToS: false, consentOnUPFCDataPolicy: false, + elineupmallFetchPurchaseHistory: false, + amebloOptimizedView: false, feedUseMemberTaggings: true, diff --git a/expo/features/artist/internals/__generated__/useUpsertFollowMutation.graphql.ts b/expo/features/artist/internals/__generated__/useUpsertFollowMutation.graphql.ts index 16273e6..c179373 100644 --- a/expo/features/artist/internals/__generated__/useUpsertFollowMutation.graphql.ts +++ b/expo/features/artist/internals/__generated__/useUpsertFollowMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<6869002b4daf4e1f7d845fb5c73fcb25>> * @lightSyntaxTransform * @nogrep */ @@ -126,168 +126,168 @@ v2 = [ "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallOther", + "name": "elineupmallBlueray", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoDaily", + "name": "elineupmallClearFile", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoA4", + "name": "elineupmallCollectionOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoA5", + "name": "elineupmallCollectionPinnapPoster", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhoto2l", + "name": "elineupmallCollectionPhoto", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoOther", + "name": "elineupmallDvd", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoAlbum", + "name": "elineupmallDvdMagazine", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoAlbumOther", + "name": "elineupmallDvdMagazineOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoBook", + "name": "elineupmallFsk", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPhotoBookOther", + "name": "elineupmallKeyringOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallDvd", + "name": "elineupmallMicrofiberTowel", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallDvdMagazine", + "name": "elineupmallMufflerTowel", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallDvdMagazineOther", + "name": "elineupmallOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallBlueray", + "name": "elineupmallPenlight", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallPenlight", + "name": "elineupmallPhotoDaily", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallCollectionPinnapPoster", + "name": "elineupmallPhotoA4", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallCollectionPhoto", + "name": "elineupmallPhotoA5", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallCollectionOther", + "name": "elineupmallPhoto2l", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallTshirt", + "name": "elineupmallPhotoOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallMicrofiberTowel", + "name": "elineupmallPhotoAlbum", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallMufflerTowel", + "name": "elineupmallPhotoAlbumOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallFsk", + "name": "elineupmallPhotoBook", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallKeyringOther", + "name": "elineupmallPhotoBookOther", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "elineupmallClearFile", + "name": "elineupmallTshirt", "storageKey": null } ], @@ -315,16 +315,16 @@ return { "selections": (v2/*: any*/) }, "params": { - "cacheID": "27074257b53a091a6263918e35e685cd", + "cacheID": "357754e0f499a9fcc492f37e71ef1677", "id": null, "metadata": {}, "name": "useUpsertFollowMutation", "operationKind": "mutation", - "text": "mutation useUpsertFollowMutation(\n $params: HPFollowUpsertParamsInput!\n) {\n me {\n upsertFollow(params: $params) {\n id\n type\n member {\n id\n }\n elineupmallOther\n elineupmallPhotoDaily\n elineupmallPhotoA4\n elineupmallPhotoA5\n elineupmallPhoto2l\n elineupmallPhotoOther\n elineupmallPhotoAlbum\n elineupmallPhotoAlbumOther\n elineupmallPhotoBook\n elineupmallPhotoBookOther\n elineupmallDvd\n elineupmallDvdMagazine\n elineupmallDvdMagazineOther\n elineupmallBlueray\n elineupmallPenlight\n elineupmallCollectionPinnapPoster\n elineupmallCollectionPhoto\n elineupmallCollectionOther\n elineupmallTshirt\n elineupmallMicrofiberTowel\n elineupmallMufflerTowel\n elineupmallFsk\n elineupmallKeyringOther\n elineupmallClearFile\n }\n }\n}\n" + "text": "mutation useUpsertFollowMutation(\n $params: HPFollowUpsertParamsInput!\n) {\n me {\n upsertFollow(params: $params) {\n id\n type\n member {\n id\n }\n elineupmallBlueray\n elineupmallClearFile\n elineupmallCollectionOther\n elineupmallCollectionPinnapPoster\n elineupmallCollectionPhoto\n elineupmallDvd\n elineupmallDvdMagazine\n elineupmallDvdMagazineOther\n elineupmallFsk\n elineupmallKeyringOther\n elineupmallMicrofiberTowel\n elineupmallMufflerTowel\n elineupmallOther\n elineupmallPenlight\n elineupmallPhotoDaily\n elineupmallPhotoA4\n elineupmallPhotoA5\n elineupmallPhoto2l\n elineupmallPhotoOther\n elineupmallPhotoAlbum\n elineupmallPhotoAlbumOther\n elineupmallPhotoBook\n elineupmallPhotoBookOther\n elineupmallTshirt\n }\n }\n}\n" } }; })(); -(node as any).hash = "8529051710ad55463849cc0462b835a8"; +(node as any).hash = "494dba2fd4a8389f13bc1168905e4855"; export default node; diff --git a/expo/features/elineupmall/ElineupMallSettingsScreen.tsx b/expo/features/elineupmall/ElineupMallSettingsScreen.tsx index ac9cbb3..6565d6a 100644 --- a/expo/features/elineupmall/ElineupMallSettingsScreen.tsx +++ b/expo/features/elineupmall/ElineupMallSettingsScreen.tsx @@ -1,13 +1,47 @@ +import { useUserConfig, useUserConfigUpdator } from '@hpapp/features/app/settings'; +import { FontSize } from '@hpapp/features/common/constants'; +import { ListItem, ListItemSwitch } from '@hpapp/features/common/list'; import { defineScreen, useScreenTitle } from '@hpapp/features/common/stack'; import { t } from '@hpapp/system/i18n'; +import { Divider } from '@rneui/themed'; +import { ScrollView, StyleSheet, Text } from 'react-native'; import ElineupMallSettingsFollowings from './internal/settings/ElineupMallSettingsFollowings'; export default defineScreen('/elineupmall/settings/', function ElineupMallSettingsScreen() { useScreenTitle(t('Elineup Mall Settings')); + const config = useUserConfig(); + const updator = useUserConfigUpdator(); return ( - <> + + + {t('Fetch Elnieup Mall Purchase History')} + + {t('FC credentials will be used to fetch your purchase history from elineupmall.')} + + + } + value={config?.elineupmallFetchPurchaseHistory ?? false} + onValueChange={(value) => { + updator({ + ...config!, + elineupmallFetchPurchaseHistory: value + }); + }} + /> + + + {t('Following settings per a member per a category')} + - + ); }); + +const styles = StyleSheet.create({ + sublabel: { + fontSize: FontSize.Small + } +}); diff --git a/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemList.tsx b/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemList.tsx index 0a1ee6f..c62444c 100644 --- a/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemList.tsx +++ b/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemList.tsx @@ -1,4 +1,5 @@ import { ListItemLoadMore } from '@hpapp/features/common/list'; +import { ElineupMallPurchaseHistoryItem } from '@hpapp/features/elineupmall/scraper'; import { useLazyReloadableQuery } from '@hpapp/system/graphql/hooks'; import { FlatList, RefreshControl } from 'react-native'; import { graphql, usePaginationFragment } from 'react-relay'; @@ -26,6 +27,7 @@ const ElineupMallLimitedTimeItemListQueryFragmentGraphQL = graphql` edges { node { id + permalink ...ElineupMallLimitedTimeItemListItemFragment } } @@ -44,12 +46,14 @@ export type ElineupMallLimitedTimeItemListProps = { }[]; memberIds: string[]; categories: ElineupMallItemCategory[]; + historyMap?: Map; }; export default function ElineupMallLimitedTimeItemList({ memberCategories, memberIds, - categories + categories, + historyMap }: ElineupMallLimitedTimeItemListProps) { const { data, isReloading, reload } = useLazyReloadableQuery( ElineupMallLimitedTimeItemListQueryGraphQL, @@ -72,7 +76,8 @@ export default function ElineupMallLimitedTimeItemList({ keyExtractor={(item) => item!.node!.id} data={histories.data.elineupMallItems!.edges} renderItem={({ item, index }) => { - return ; + const historyItem = historyMap?.get(item?.node?.permalink ?? ''); + return ; }} onEndReachedThreshold={0.01} onEndReached={() => { diff --git a/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemListItem.tsx b/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemListItem.tsx index 2a94ff3..371ae11 100644 --- a/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemListItem.tsx +++ b/expo/features/elineupmall/internal/ElineupMallLimitedTimeItemListItem.tsx @@ -5,6 +5,7 @@ import { FontSize, Spacing } from '@hpapp/features/common/constants'; import { ListItem } from '@hpapp/features/common/list'; import { useNavigation } from '@hpapp/features/common/stack'; import ElineupMallWebViewScreen from '@hpapp/features/elineupmall/ElineupMallWebViewScreen'; +import { ElineupMallPurchaseHistoryItem } from '@hpapp/features/elineupmall/scraper'; import * as date from '@hpapp/foundation/date'; import { t } from '@hpapp/system/i18n'; import { Divider } from '@rneui/base'; @@ -31,7 +32,13 @@ const ElineupMallLimitedTimeItemListItemFragmentGraphQL = graphql` } `; -export function ElineupMallLimitedTimeItemListItem({ data }: { data: ElineupMallLimitedTimeItemListItemFragment$key }) { +export function ElineupMallLimitedTimeItemListItem({ + data, + purchaseHistoryItem +}: { + data: ElineupMallLimitedTimeItemListItemFragment$key; + purchaseHistoryItem?: ElineupMallPurchaseHistoryItem; +}) { const [color, contrast] = useThemeColor('primary'); const navigation = useNavigation(); const item = useFragment( @@ -39,6 +46,8 @@ export function ElineupMallLimitedTimeItemListItem({ data }: { data: ElineupMall data ); const dateString = date.toDateString(item.orderEndAt); + const orderedAt = + purchaseHistoryItem !== undefined ? date.toDateString(purchaseHistoryItem!.order.orderedAt) : undefined; const imageUrl = item.images[0].url; return ( <> @@ -66,7 +75,7 @@ export function ElineupMallLimitedTimeItemListItem({ data }: { data: ElineupMall - {t('Order End At')} + {t('Order End On')} {dateString} @@ -80,6 +89,24 @@ export function ElineupMallLimitedTimeItemListItem({ data }: { data: ElineupMall {t('%{price} JPY', { price: item.price })} + {orderedAt && ( + + + {t('Ordered On')} + + + {orderedAt} + + + )} diff --git a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery.graphql.ts b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery.graphql.ts index 0dc7f52..9ff933b 100644 --- a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery.graphql.ts +++ b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -153,14 +153,14 @@ return { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "permalink", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "permalink", + "name": "name", "storageKey": null }, { @@ -296,12 +296,12 @@ return { ] }, "params": { - "cacheID": "be25bc7b64118ca4dea7c73715acf882", + "cacheID": "6230e7307cd9db4e0eb95921d7dcf662", "id": null, "metadata": {}, "name": "ElineupMallLimitedTimeItemListQuery", "operationKind": "query", - "text": "query ElineupMallLimitedTimeItemListQuery(\n $first: Int\n $after: Cursor\n $params: HPElineumpMallItemsParamsInput!\n) {\n helloproject {\n ...ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items\n id\n }\n}\n\nfragment ElineupMallLimitedTimeItemListItemFragment on HPElineupMallItem {\n id\n name\n permalink\n description\n price\n isLimitedToFc\n isOutOfStock\n images {\n url\n }\n category\n orderStartAt\n orderEndAt\n}\n\nfragment ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items on HelloProjectQuery {\n elineupMallItems(first: $first, after: $after, params: $params) {\n edges {\n node {\n id\n ...ElineupMallLimitedTimeItemListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" + "text": "query ElineupMallLimitedTimeItemListQuery(\n $first: Int\n $after: Cursor\n $params: HPElineumpMallItemsParamsInput!\n) {\n helloproject {\n ...ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items\n id\n }\n}\n\nfragment ElineupMallLimitedTimeItemListItemFragment on HPElineupMallItem {\n id\n name\n permalink\n description\n price\n isLimitedToFc\n isOutOfStock\n images {\n url\n }\n category\n orderStartAt\n orderEndAt\n}\n\nfragment ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items on HelloProjectQuery {\n elineupMallItems(first: $first, after: $after, params: $params) {\n edges {\n node {\n id\n permalink\n ...ElineupMallLimitedTimeItemListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" } }; })(); diff --git a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQueryFragmentQuery.graphql.ts b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQueryFragmentQuery.graphql.ts index 5ef6428..0452003 100644 --- a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQueryFragmentQuery.graphql.ts +++ b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQueryFragmentQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<59cd9c9ada3be5510cc0fa48faa0d869>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -180,14 +180,14 @@ return { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "permalink", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "permalink", + "name": "name", "storageKey": null }, { @@ -320,16 +320,16 @@ return { ] }, "params": { - "cacheID": "8b37ef7184e909e27c6cbada23ffb83d", + "cacheID": "64589004f7e17e01c5f0fd646a98c7e3", "id": null, "metadata": {}, "name": "ElineupMallLimitedTimeItemListQueryFragmentQuery", "operationKind": "query", - "text": "query ElineupMallLimitedTimeItemListQueryFragmentQuery(\n $after: Cursor\n $first: Int\n $params: HPElineumpMallItemsParamsInput!\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items\n id\n }\n}\n\nfragment ElineupMallLimitedTimeItemListItemFragment on HPElineupMallItem {\n id\n name\n permalink\n description\n price\n isLimitedToFc\n isOutOfStock\n images {\n url\n }\n category\n orderStartAt\n orderEndAt\n}\n\nfragment ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items on HelloProjectQuery {\n elineupMallItems(first: $first, after: $after, params: $params) {\n edges {\n node {\n id\n ...ElineupMallLimitedTimeItemListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" + "text": "query ElineupMallLimitedTimeItemListQueryFragmentQuery(\n $after: Cursor\n $first: Int\n $params: HPElineumpMallItemsParamsInput!\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items\n id\n }\n}\n\nfragment ElineupMallLimitedTimeItemListItemFragment on HPElineupMallItem {\n id\n name\n permalink\n description\n price\n isLimitedToFc\n isOutOfStock\n images {\n url\n }\n category\n orderStartAt\n orderEndAt\n}\n\nfragment ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items on HelloProjectQuery {\n elineupMallItems(first: $first, after: $after, params: $params) {\n edges {\n node {\n id\n permalink\n ...ElineupMallLimitedTimeItemListItemFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n" } }; })(); -(node as any).hash = "f2e59726532c2bac145a183dbedebed4"; +(node as any).hash = "51b8104b42d9cf3b86c217b2478fc028"; export default node; diff --git a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items.graphql.ts b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items.graphql.ts index c90b799..8998144 100644 --- a/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items.graphql.ts +++ b/expo/features/elineupmall/internal/__generated__/ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_items.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<71b338e3ace4d6b1a3f3b11341ab34b7>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,7 @@ export type ElineupMallLimitedTimeItemListQuery_helloproject_query_elineupmall_i readonly edges: ReadonlyArray<{ readonly node: { readonly id: string; + readonly permalink: string; readonly " $fragmentSpreads": FragmentRefs<"ElineupMallLimitedTimeItemListItemFragment">; } | null | undefined; } | null | undefined> | null | undefined; @@ -115,6 +116,13 @@ return { "plural": false, "selections": [ (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "permalink", + "storageKey": null + }, { "args": null, "kind": "FragmentSpread", @@ -175,6 +183,6 @@ return { }; })(); -(node as any).hash = "f2e59726532c2bac145a183dbedebed4"; +(node as any).hash = "51b8104b42d9cf3b86c217b2478fc028"; export default node; diff --git a/expo/features/elineupmall/internal/settings/ElineupMallSettingsFollowings.tsx b/expo/features/elineupmall/internal/settings/ElineupMallSettingsFollowings.tsx index f17156c..f0cda07 100644 --- a/expo/features/elineupmall/internal/settings/ElineupMallSettingsFollowings.tsx +++ b/expo/features/elineupmall/internal/settings/ElineupMallSettingsFollowings.tsx @@ -1,5 +1,4 @@ import { useHelloProject } from '@hpapp/features/app/user'; -import { ScrollView } from 'react-native'; import ElineupMallSettingsFollowingMember from './ElineupMallSettingsFollowingMember'; @@ -7,10 +6,10 @@ export default function ElineupMallSettingsFollowings() { const followings = useHelloProject()!.useFollowingMembers(false); return ( - + <> {followings.map((member) => ( ))} - + ); } diff --git a/expo/features/elineupmall/scraper/index.ts b/expo/features/elineupmall/scraper/index.ts new file mode 100644 index 0000000..4aa798c --- /dev/null +++ b/expo/features/elineupmall/scraper/index.ts @@ -0,0 +1,6 @@ +import useElineupMallPurchaseHistory, { + ElineupMallPurchaseHistoryLoadingStatus, + ElineupMallPurchaseHistoryItem +} from './internals/useElineupMallPurchaseHistory'; + +export { useElineupMallPurchaseHistory, ElineupMallPurchaseHistoryLoadingStatus, ElineupMallPurchaseHistoryItem }; diff --git a/expo/features/elineupmall/scraper/internals/ElineupMallFileFetcher.ts b/expo/features/elineupmall/scraper/internals/ElineupMallFileFetcher.ts new file mode 100644 index 0000000..01a0cb8 --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/ElineupMallFileFetcher.ts @@ -0,0 +1,38 @@ +import { readFile } from '@hpapp/foundation/test_helper'; + +import { ElineupMallFetcher } from './types'; + +/** + * an ElineupMallFileFetcher fetches HTML files from the file system. + */ +export default class ElineupMallFileFetcher implements ElineupMallFetcher { + private paths: { + authResultHTMLPath: string; + orderListHTMLPath: string; + orderDetailHTMLPath: string; + }; + + constructor(paths: { authResultHTMLPath: string; orderListHTMLPath: string; orderDetailHTMLPath: string }) { + this.paths = paths; + } + + async postCredential(username: string, password: string): Promise { + return await this.readFile(this.paths.authResultHTMLPath); + } + + async fetchOrderListHtml(page: number): Promise { + return await this.readFile(this.paths.orderListHTMLPath); + } + + async fetchOrderDetailHtml(id: string): Promise { + return await this.readFile(this.paths.orderDetailHTMLPath); + } + + async readFile(path: string): Promise { + try { + return await readFile(path); + } catch { + return ''; + } + } +} diff --git a/expo/features/elineupmall/scraper/internals/ElineupMallHttpFetcher.ts b/expo/features/elineupmall/scraper/internals/ElineupMallHttpFetcher.ts new file mode 100644 index 0000000..c31bee9 --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/ElineupMallHttpFetcher.ts @@ -0,0 +1,69 @@ +import { ElineupMallFetcher } from './types'; + +/** + * a ElineupMallHttpFetcher fetches html from elineupmall.com + */ +export default class ElineupMallHttpFetcher implements ElineupMallFetcher { + /** + * post a credential to up-fc.jp + * @param username + * @param password + * @returns + */ + async postCredential(username: string, password: string): Promise { + const url = `https://www.elineupmall.com/`; + + await this.fetch(url); // dummy request to generate session + const params = new URLSearchParams(); + params.append('return_url', 'index.php'); + params.append('redirect_url', 'index.php'); + params.append('user_login', username); + params.append('password', password); + params.append('dispatch[auth.login]', ''); + params.append('remember_me', 'Y'); + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' + }, + body: params.toString(), + credentials: 'include' + }); + return await resp.text(); + } + + async fetchOrderListHtml(page: number): Promise { + return await this.fetch(`https://www.elineupmall.com/orders/?page=${page}`); + } + + async fetchOrderDetailHtml(id: string): Promise { + return await this.fetch(`https://www.elineupmall.com/index.php?dispatch=orders.details&order_id=${id}`); + } + + async fetch(url: string): Promise { + const resp = await fetch(url, { + credentials: 'include' + }); + if (!resp.ok) { + throw new Error('invalid response'); + } + const html = await resp.text(); + return html; + } + + async postForm(url: string, body: string): Promise { + const resp = await fetch(url, { + method: 'POST', + credentials: 'include', + body, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + } + }); + if (!resp.ok) { + throw new Error('invalid response'); + } + const html = await resp.text(); + return html; + } +} diff --git a/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.test.ts b/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.test.ts new file mode 100644 index 0000000..7229e43 --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.test.ts @@ -0,0 +1,68 @@ +import * as path from 'path'; + +import ElineupMallFileFetcher from './ElineupMallFileFetcher'; +import ElineupMallSiteScraper from './ElineupMallSiteScraper'; + +describe('ElineupMallSiteScraper', () => { + describe('authenticate', () => { + it('success', async () => { + const scraper = new ElineupMallSiteScraper( + new ElineupMallFileFetcher({ + authResultHTMLPath: path.join(__dirname, './testdata/auth_success.html'), + orderListHTMLPath: '', + orderDetailHTMLPath: '' + }) + ); + const ok = await scraper.authenticate('mizuki', 'fukumura'); + expect(ok).toBe(true); + }); + + it('error', async () => { + const scraper = new ElineupMallSiteScraper( + new ElineupMallFileFetcher({ + authResultHTMLPath: path.join(__dirname, './testdata/auth_error.html'), + orderListHTMLPath: '', + orderDetailHTMLPath: '' + }) + ); + const ok = await scraper.authenticate('mizuki', 'fukumura'); + expect(ok).toBe(false); + }); + }); + + it('order_list', async () => { + const scraper = new ElineupMallSiteScraper( + new ElineupMallFileFetcher({ + authResultHTMLPath: '', + orderListHTMLPath: path.join(__dirname, './testdata/order_list.html'), + orderDetailHTMLPath: path.join(__dirname, './testdata/order_detail.html') + }) + ); + const list = await scraper.getOrderList(new Date('2024-08-04T00:00:00+09:00')); + expect(list.length).toBe(3); + expect(list[0].id).toBe('1430811'); + expect(list[0].status).toBe('payment_confirmed'); + expect(list[0].orderedAt.toISOString()).toBe('2024-12-23T07:33:00.000Z'); + expect(list[1].id).toBe('1397584'); + expect(list[1].status).toBe('payment_confirmed'); + expect(list[1].orderedAt.toISOString()).toBe('2024-10-20T08:58:00.000Z'); + expect(list[2].id).toBe('1375993'); + expect(list[2].status).toBe('shipped'); + expect(list[2].orderedAt.toISOString()).toBe('2024-08-30T09:22:00.000Z'); + + expect(list[0].details.length).toBe(1); + expect(list[0].details[0].name).toBe( + '2024年12月通信販売 DVD「Juice=Juice 工藤由愛バースデーイベント2024 /Juice=Juice 有澤一華・入江里咲・江端妃咲FCイベント2024」' + ); + expect(list[0].details[0].name).toBe( + '2024年12月通信販売 DVD「Juice=Juice 工藤由愛バースデーイベント2024 /Juice=Juice 有澤一華・入江里咲・江端妃咲FCイベント2024」' + ); + expect(list[0].details[0].unitPrice).toBe(8250); + expect(list[0].details[0].num).toBe(1); + expect(list[0].details[0].totalPrice).toBe(8250); + expect(list[0].details[0].code).toBe('HP2412_H-03'); + expect(list[0].details[0].link).toBe( + 'https://www.elineupmall.com/c720/c2784/202412-dvdjuicejuice-2024-juicejuice-fc2024-alp84jmu/' + ); + }); +}); diff --git a/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.ts b/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.ts new file mode 100644 index 0000000..324fa2d --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/ElineupMallSiteScraper.ts @@ -0,0 +1,128 @@ +import { parse } from 'node-html-parser'; + +import { + ElineupMallFetcher, + ElineupMallOrder, + ElineupMallOrderDetail, + ElineupMallOrderStatus, + ElineupMallScraper +} from './types'; + +/** + * a ElineupMallSiteScraper scrape html for elineupmall.com + */ +export default class ElineupMallSiteScraper implements ElineupMallScraper { + private readonly fetcher: ElineupMallFetcher; + + constructor(fetcher: ElineupMallFetcher) { + this.fetcher = fetcher; + } + + public async authenticate(username: string, password: string): Promise { + const html = await this.fetcher.postCredential(username, password); + const root = parse(html); + const text = root.querySelector('div.cm-notification-container')?.text; + if (text === undefined) { + return false; + } + return text.indexOf('ログインに成功') >= 0; + } + + public async getOrderList(from: Date): Promise { + const html = await this.fetcher.fetchOrderListHtml(1); + const root = parse(html); + let orders = root.querySelectorAll('table.ty-orders-search > tr').map((row) => { + const id = row.querySelector('td:nth-child(1)')?.text.trim().replace('#', ''); + const status = toOrderStatus(row.querySelector('td:nth-child(2)')?.text.trim()); + const orderedAt = new Date( + row.querySelector('td:nth-child(4)')?.text.trim().replaceAll('/', '-').replace(', ', 'T') + ':00+09:00' + ); + return { + id: id ?? '', + status, + orderedAt, + details: [] + } as ElineupMallOrder; + }); + orders = orders.filter((order) => { + return order.id !== '' && order.status !== 'unknown' && order.orderedAt.getTime() >= from.getTime(); + }); + orders = await Promise.all( + orders.map(async (o) => { + o.details = await this.fetchOrderDetail(o.id); + return o; + }) + ); + return orders.filter((order) => order.details?.length > 0); + } + + private async fetchOrderDetail(id: string): Promise { + const html = await this.fetcher.fetchOrderDetailHtml(id); + const root = parse(html); + const details: ElineupMallOrderDetail[] = root.querySelectorAll('table.ty-orders-detail__table> tr').map((row) => { + const a = row.querySelector('td:nth-child(1) > a'); + if (a === null) { + return { + name: '', + num: 0, + link: '', + code: '', + unitPrice: 0, + totalPrice: 0 + }; + } + const name = a.text.trim(); + const link = a.getAttribute('href') ?? ''; + const code = + row + .querySelector('td:nth-child(1) > div.ty-orders-detail__table-code') + ?.text.replaceAll(' ', '') + .replace('コード:', '') + .trim() ?? ''; + const unitPrice = parseInt( + row + .querySelector('td:nth-child(2)') + ?.text.replaceAll(' ', '') + .replaceAll(',', '') + .replace('円', '') + .trim() ?? '0', + 10 + ); + const num = parseInt(row.querySelector('td:nth-child(3)')?.text.replaceAll(' ', '').trim() ?? '0', 10); + return { + name, + num, + link, + code, + unitPrice, + totalPrice: unitPrice * num + }; + }); + return details.filter((d) => { + return d.name !== '' && d.num > 0 && d.unitPrice > 0; + }); + } +} + +function toOrderStatus(statusString: string | undefined): ElineupMallOrderStatus { + switch (statusString) { + case '支払い確認済み': + return 'payment_confirmed'; + case '発送済み': + return 'shipped'; + case '注文受付': + return 'ordered'; + case '失敗': + return 'failed'; + case '拒否': + return 'denied'; + case '在庫切れ': + return 'out_of_stock'; + case 'キャンセル': + return 'canceled'; + case '注文未処理': + return 'unprocessed'; + default: + return 'unknown'; + } +} diff --git a/expo/features/elineupmall/scraper/internals/testdata/auth_error.html b/expo/features/elineupmall/scraper/internals/testdata/auth_error.html new file mode 100644 index 0000000..2cf5c0d --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/testdata/auth_error.html @@ -0,0 +1,7968 @@ + + + +e-LineUP!Mall(イーラインナップモール) -ショッピングモール- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ + エラー + ユーザー名もしくはパスワードが正しくありません。もう一度入力してください。 +
+
+ +
+ + + + +
+
+ + + +
+
+

+
+ + +
+ +
+ + +
+ + + +
+
+
+
+
+ + +
+
+ +
+ + + +
+
+
+
+
+
+
+
+ + +
+ +
+ + + +
+
+ +
+ + +
+ + + +
+
+
+ + + + + + +
+
+ +
+
+ + + +
+
+
    +
  • +
    +

    海外発送/国際配送サービスの転送コム

    +
    +
  • +
  • +
    +
     
    + +
    +
  • +
  • +
    +

    海外発送/代理購入サービスのBuyee

    +
    +
  • +
  • +
    + + +
     
    + +
    +
  • +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+
+ + + + +
+
+
+ + +
+
+ + +
+

+ + + + +

+
+
+ + +
    +
  • + +
  • + +
  • + +
+
+
+ +

+ + 新着商品(TOP) + + +

+ + +
+ + + +
+
+
+
+
+
+
+
+ + + + + + + + +
+
+ +

+ + 都道府県検索 + + +

+ + +
+
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+ + + + + + + + + +
+
北海道
+
北海道
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
東北
+
青森県岩手県
宮城県秋田県
山形県福島県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
関東・甲信越
+
茨城県栃木県
群馬県埼玉県
千葉県東京都
神奈川県新潟県
山梨県長野県
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
北陸・東海
+
富山県石川県
福井県岐阜県
静岡県愛知県
三重県
+
+ +
+ + + + + + + + + + + + + + + + + +
+
近畿
+
滋賀県京都府
大阪府兵庫県
奈良県和歌山県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
中国・四国
+
鳥取県島根県
岡山県広島県
山口県徳島県
香川県愛媛県
高知県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
九州・沖縄
+
福岡県佐賀県
長崎県熊本県
大分県宮崎県
鹿児島県沖縄県
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+ + + + +
+
+
+
+
+
+
+ + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/expo/features/elineupmall/scraper/internals/testdata/auth_success.html b/expo/features/elineupmall/scraper/internals/testdata/auth_success.html new file mode 100644 index 0000000..39ee13a --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/testdata/auth_success.html @@ -0,0 +1,7872 @@ + + + +e-LineUP!Mall(イーラインナップモール) -ショッピングモール- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ + お知らせ + ログインに成功しました。 +
+
+ +
+ + + + +
+
+ + + +
+
+

+
+ + +
+ +
+ + +
+ + + +
+
+
+
+
+ + +
+
+ +
+ + + +
+
+
+
+
+
+
+
+ + +
+ +
+ + + +
+
+ +
+ + +
+ + + +
+
+
+ + + + + + +
+
+ +
+
+ + + +
+
+
    +
  • +
    +

    海外発送/国際配送サービスの転送コム

    +
    +
  • +
  • +
    +
     
    + +
    +
  • +
  • +
    +

    海外発送/代理購入サービスのBuyee

    +
    +
  • +
  • +
    + + +
     
    + +
    +
  • +
+
+ +
+
+
+ + +
+ +
+ + + +
+
+
+ + + + +
+
+
+ + +
+
+ + +
+

+ + + + +

+
+
+ + +
    +
  • + +
  • + +
  • + +
+
+
+ +

+ + 新着商品(TOP) + + +

+ + +
+ + + +
+
+
+
+
+
+
+
+ + + + + + + + +
+
+ +

+ + 都道府県検索 + + +

+ + +
+
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+
+ + +
+
+
+ +
+
+
+ +
+
+ +
+ + + + + + + + + +
+
北海道
+
北海道
+
+ +
+ + + + + + + + + + + + + + + + + + +
+
東北
+
青森県岩手県
宮城県秋田県
山形県福島県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
関東・甲信越
+
茨城県栃木県
群馬県埼玉県
千葉県東京都
神奈川県新潟県
山梨県長野県
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
北陸・東海
+
富山県石川県
福井県岐阜県
静岡県愛知県
三重県
+
+ +
+ + + + + + + + + + + + + + + + + +
+
近畿
+
滋賀県京都府
大阪府兵庫県
奈良県和歌山県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
中国・四国
+
鳥取県島根県
岡山県広島県
山口県徳島県
香川県愛媛県
高知県
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
九州・沖縄
+
福岡県佐賀県
長崎県熊本県
大分県宮崎県
鹿児島県沖縄県
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+ + + + +
+
+
+
+
+
+
+ + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/expo/features/elineupmall/scraper/internals/testdata/order_detail.html b/expo/features/elineupmall/scraper/internals/testdata/order_detail.html new file mode 100644 index 0000000..7a1fd32 --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/testdata/order_detail.html @@ -0,0 +1,1199 @@ + + + + + +注文 :: 注文情報 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ +
+ + + + +
+
+ + + +
+
+

+
+ + +
+ +
+ + +
+ + + +
+
+
+
+
+ + +
+
+ +
+ + + +
+
+
+
+
+
+
+
+ + +
+ +
+ + + +
+
+ +
+ + +
+ + + +
+
+
+ + + + + + +
+
+ +
+
+ + + +
+
+ + +
+
+ +

+ + 注文 #1430811 + (2024/12/23, 16:33) + ステータス: +支払い確認済み + + + +

+ + +
+
+ + + + + +
+ + + + + 領収書を印刷 + + +   ※課税対象商品が含まれていない場合は領収書は表示されません。 + + + + + +
+ + + + + + + この商品を再注文する + + +
+ +
+ +
+ +
+ +
+ +
+ + +
+

+ + お客様情報 + +

+ + +
+
+
請求先住所
+
+
+
+
+
配送先住所
+
+
+ +
+ + + +
+
+
+

+ + 商品情報 + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
商品価格数量小計
+ 2024年12月通信販売 DVD「Juice=Juice 工藤由愛バースデーイベント2024 /Juice=Juice 有澤一華・入江里咲・江端妃咲FCイベント2024」 + +
コード: HP2412_H-03
+ + + +
+ 8,250 円  1 +  8,250 円
+ + + + +
+

+ + 概要 + +

+ +
+ +
BarCode
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
支払方法: + + クレジットカード(登録済カード決済) (登録したクレジットカードを使う場合セキュリティコードのみで決済できます。) + +
配送方法: + +
    +
  • 佐川急便
  • + + + + +
+ + +
小計: 8,250 円
送料: 800 円
税金: 
+ 消費税 + 823 円
合計(税込): 9,050 円
+
+
+ + +
+ + + +
+ +
+
+
+
+ + + + + + + + +
+ + + +
+ + + + + +
+
+
+
+
+
+ + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/expo/features/elineupmall/scraper/internals/testdata/order_list.html b/expo/features/elineupmall/scraper/internals/testdata/order_list.html new file mode 100644 index 0000000..6f9fd05 --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/testdata/order_list.html @@ -0,0 +1,2154 @@ + + + + 注文 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ +
+
+
+
+
+
+
+

+ +

+
+
+
+ + +
+
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ + + +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+ +
+
+
+

注文

+ +
+
+
+ 検索オプション + 表示 + 非表示 +
+
+
+
+ +  –  +
+ +
+ + +
+ + + + + + + + +
+
+ +
+
+ + +
+ +
+ + +
+ + + + +
+ + + + +
+ + + + +
+ + +
+ + + +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+
+  前へ + +
+ 1 + 2 + 3 + 4 +
+ + 次へ  +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+  前へ + +
+ 1 + 2 + 3 + 4 +
+ + 次へ  +
+
+ + +
+
+
+
+
+
+
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/expo/features/elineupmall/scraper/internals/types.ts b/expo/features/elineupmall/scraper/internals/types.ts new file mode 100644 index 0000000..1c2948c --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/types.ts @@ -0,0 +1,37 @@ +export type ElineupMallOrderStatus = + | 'payment_confirmed' + | 'shipped' + | 'ordered' + | 'failed' + | 'denied' + | 'out_of_stock' + | 'canceled' + | 'unprocessed' + | 'unknown'; + +export type ElineupMallOrder = { + id: string; + status: ElineupMallOrderStatus; + orderedAt: Date; + details: ElineupMallOrderDetail[]; +}; + +export type ElineupMallOrderDetail = { + name: string; + num: number; + link: string; + code: string; + unitPrice: number; + totalPrice: number; +}; + +export interface ElineupMallScraper { + authenticate(username: string, password: string): Promise; + getOrderList(from: Date): Promise; +} + +export interface ElineupMallFetcher { + postCredential(username: string, password: string): Promise; + fetchOrderListHtml(pageNum: number): Promise; + fetchOrderDetailHtml(id: string): Promise; +} diff --git a/expo/features/elineupmall/scraper/internals/useElineupMallPurchaseHistory.ts b/expo/features/elineupmall/scraper/internals/useElineupMallPurchaseHistory.ts new file mode 100644 index 0000000..dfea82d --- /dev/null +++ b/expo/features/elineupmall/scraper/internals/useElineupMallPurchaseHistory.ts @@ -0,0 +1,68 @@ +import { useUPFCConfig, useUserConfig } from '@hpapp/features/app/settings'; +import * as date from '@hpapp/foundation/date'; +import { isEmpty } from '@hpapp/foundation/string'; +import * as logging from '@hpapp/system/logging'; +import { useEffect, useMemo, useState } from 'react'; + +import ElineupMallHttpFetcher from './ElineupMallHttpFetcher'; +import ElineupMallSiteScraper from './ElineupMallSiteScraper'; +import { ElineupMallOrder, ElineupMallOrderDetail } from './types'; + +export type ElineupMallPurchaseHistoryItem = ElineupMallOrderDetail & { order: ElineupMallOrder }; + +export type ElineupMallPurchaseHistoryLoadingStatus = + | 'loaded' + | 'loading' + | 'error_not_opted_in' + | 'error_upfc_is_empty' + | 'error_fetch'; + +const scraper = new ElineupMallSiteScraper(new ElineupMallHttpFetcher()); + +export default function useElineupMallPurchaseHistory(): [ + Map, + ElineupMallPurchaseHistoryLoadingStatus +] { + const [history, setHistory] = useState([]); + const [status, setStatus] = useState('loaded'); + const userConfig = useUserConfig(); + const upfcConfig = useUPFCConfig(); + useEffect(() => { + if (!userConfig?.elineupmallFetchPurchaseHistory) { + setStatus('error_not_opted_in'); + return; + } + if (isEmpty(upfcConfig?.hpUsername) || isEmpty(upfcConfig?.hpPassword)) { + setStatus('error_upfc_is_empty'); + } + setStatus('loading'); + (async () => { + try { + scraper.authenticate(upfcConfig!.hpUsername!, upfcConfig!.hpPassword!); + const from = date.addDate(date.getToday(), -180, 'day'); + const orderList = await scraper.getOrderList(from); + logging.Info('features.elineupmall.scraper.internals.useElineupMallPurshaseHistory', 'completed', { + num: orderList.length + }); + // TODO: sync with server + setHistory(orderList); + setStatus('loaded'); + } catch (e: unknown) { + logging.Info('features.elineupmall.scraper.internals.useElineupMallPurshaseHistory', 'failed', { + error: (e as any).toString() + }); + setStatus('error_fetch'); + } + })(); + }, [userConfig?.elineupmallFetchPurchaseHistory, upfcConfig?.hpUsername, upfcConfig?.hpPassword]); + const historyMap = useMemo(() => { + const map = new Map(); + for (const order of history) { + for (const detail of order.details) { + map.set(detail.link, { ...detail, order }); + } + } + return map; + }, [history]); + return [historyMap, status]; +} diff --git a/expo/features/home/internals/HomeTabElineupMallProvider.tsx b/expo/features/home/internals/HomeTabElineupMallProvider.tsx new file mode 100644 index 0000000..9b19219 --- /dev/null +++ b/expo/features/home/internals/HomeTabElineupMallProvider.tsx @@ -0,0 +1,16 @@ +import { useElineupMallPurchaseHistory } from '@hpapp/features/elineupmall/scraper'; +import { createContext, useContext } from 'react'; + +export type ElineupMallContext = ReturnType; + +const elineupMallContext = createContext(null); + +export default function HomeTabElineupMallProvider({ children }: { children: React.ReactElement }) { + const result = useElineupMallPurchaseHistory(); + return {children}; +} + +export function useHomeTabElineupMall() { + const value = useContext(elineupMallContext); + return value!; +} diff --git a/expo/features/home/internals/HomeTabProvider.tsx b/expo/features/home/internals/HomeTabProvider.tsx index 5c1067b..1e25a27 100644 --- a/expo/features/home/internals/HomeTabProvider.tsx +++ b/expo/features/home/internals/HomeTabProvider.tsx @@ -2,6 +2,7 @@ import { useHelloProject } from '@hpapp/features/app/user'; import { createFeedContext } from '@hpapp/features/feed'; import { useMemo } from 'react'; +import HomeTabElineupMallProvider, { useHomeTabElineupMall } from './HomeTabElineupMallProvider'; import HomeTabUPFCProvider, { useHomeTabUPFC } from './HomeTabUPFCProvider'; const [HomeTabFeedProvider, useHomeTabFeed] = createFeedContext(); @@ -12,7 +13,9 @@ export default function HomeTabProvider({ children }: { children: React.ReactEle .map((m) => m.id); return ( - {children} + + {children} + ); } @@ -20,10 +23,12 @@ export default function HomeTabProvider({ children }: { children: React.ReactEle export function useHomeTabContext() { const feed = useHomeTabFeed(); const upfc = useHomeTabUPFC(); + const elineupMall = useHomeTabElineupMall; return useMemo(() => { return { feed, - upfc + upfc, + elineupMall }; - }, [feed, upfc]); + }, [feed, upfc, elineupMall]); } diff --git a/expo/features/home/internals/goods/HomeTabGoods.tsx b/expo/features/home/internals/goods/HomeTabGoods.tsx index 7b5493b..082311e 100644 --- a/expo/features/home/internals/goods/HomeTabGoods.tsx +++ b/expo/features/home/internals/goods/HomeTabGoods.tsx @@ -5,6 +5,9 @@ import { ElineupMallNoFollowingsBox } from '@hpapp/features/elineupmall'; +import HomeTabGoodsElineupMallStatusHeader from './HomeTabGoodsElineupMallStatusHeader'; +import { useHomeTabElineupMall } from '../HomeTabElineupMallProvider'; + export default function HomeTabGoods() { const memberCategories = useHelloProject()! .useFollowingMembers(true) @@ -88,10 +91,21 @@ export default function HomeTabGoods() { }; }) .filter((v) => v.categories.length > 0); + const [historyMap, status] = useHomeTabElineupMall(); if (memberCategories.length === 0) { return ; } - return ; + return ( + <> + + + + ); } function isFollwoingCategory(value: HPFollowType | undefined) { diff --git a/expo/features/home/internals/goods/HomeTabGoodsElineupMallStatusHeader.tsx b/expo/features/home/internals/goods/HomeTabGoodsElineupMallStatusHeader.tsx new file mode 100644 index 0000000..c74979d --- /dev/null +++ b/expo/features/home/internals/goods/HomeTabGoodsElineupMallStatusHeader.tsx @@ -0,0 +1,78 @@ +import { useThemeColor } from '@hpapp/features/app/theme'; +import { IconSize, Spacing } from '@hpapp/features/common/constants'; +import { ElineupMallPurchaseHistoryLoadingStatus } from '@hpapp/features/elineupmall/scraper'; +import { Button, Icon } from '@rneui/themed'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; + +export type HomeTabGoodsElineupMallStatusHeaderPros = { + status: ElineupMallPurchaseHistoryLoadingStatus; +}; + +export default function HomeTabGoodsElineupMallStatusHeader({ status }: HomeTabGoodsElineupMallStatusHeaderPros) { + const [successColor] = useThemeColor('success'); + const [errorColor] = useThemeColor('error'); + const [disabledColor] = useThemeColor('disabled'); + const succsesIcon = ( + + ); + const errorIcon = ( + + ); + const disabledIcon = ( + + ); + const loadingIcon = ; + return ( + +